@owney/sdk 0.3.5 → 0.4.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/README.md CHANGED
@@ -298,11 +298,17 @@ console.log(allApy.apyByChainAndAsset[42161]?.USDT); // 4.7
298
298
 
299
299
  Get transaction history for a specific agent, or all agents. When querying all agents, results are merged into a single list sorted by date (newest first). Each entry includes the originating `agent` and a `transactions` array with typed on-chain transaction details.
300
300
 
301
- | Param | Type | Description |
302
- | ------------------ | ------------------------------ | ------------------------------------ |
303
- | `agentId` | `"zyfai" \| "sail"` (optional) | Agent to query. Omit for all agents. |
304
- | `filters.fromDate` | `string` (optional) | Start date (`YYYY-MM-DD`) |
305
- | `filters.toDate` | `string` (optional) | End date (`YYYY-MM-DD`) |
301
+ Results are **cursor-paginated**. The first call (no `cursor`) anchors a stable snapshot at the current time; subsequent pages within that same snapshot are consistent. Each new cursorless call captures a fresh snapshot — if you need to preserve scroll position while paging, always carry forward the cursor returned by the previous call rather than starting a new cursorless fetch.
302
+
303
+ > **Per-chain filtering:** The Zyfai backend no longer accepts a `chainId` filter directly. The SDK performs a bounded internal drain (up to 5 backend round-trips per logical page) to fill the requested `limit` with entries matching the active chain. This is transparent to callers — the `getHistory` API is unchanged.
304
+
305
+ | Param | Type | Description |
306
+ | ------------------ | ------------------------------ | -------------------------------------------------------- |
307
+ | `agentId` | `"zyfai" \| "sail"` (optional) | Agent to query. Omit for all agents. |
308
+ | `filters.fromDate` | `string` (optional) | Start date (`YYYY-MM-DD`) |
309
+ | `filters.toDate` | `string` (optional) | End date (`YYYY-MM-DD`) |
310
+ | `filters.limit` | `number` (optional) | Max entries returned per call. Defaults to `10`. |
311
+ | `filters.cursor` | `string` (optional) | Opaque cursor returned by a previous `getHistory` call. |
306
312
 
307
313
  Returns: `OwneyAgentHistory`
308
314
 
@@ -312,7 +318,7 @@ const history = await sdk.getHistory({
312
318
  agentId: "zyfai",
313
319
  filters: { fromDate: "2025-01-01" },
314
320
  });
315
- console.log(history.total); // 42
321
+ console.log(history.hasMore); // true | false
316
322
  console.log(history.data[0].agent); // "zyfai"
317
323
  console.log(history.data[0].action); // "Rebalance"
318
324
  console.log(history.data[0].date); // "2025-03-15T12:00:00Z"
@@ -329,9 +335,20 @@ console.log(history.data[0].rebalanceLog[0].status); // "success"
329
335
 
330
336
  // All agents (merged and sorted by date)
331
337
  const allHistory = await sdk.getHistory();
332
- console.log(allHistory.total); // 57
338
+ console.log(allHistory.hasMore); // true | false
333
339
  console.log(allHistory.data[0].agent); // "sail" (most recent entry)
334
340
  console.log(allHistory.data[0].action); // "Rebalance"
341
+
342
+ // Paginate through all history (per-chain, anchored to a single snapshot)
343
+ const allEntries: AgentHistoryEntry[] = [];
344
+ let cursor: string | undefined;
345
+ do {
346
+ const page = await sdk.getHistory({
347
+ filters: { cursor, limit: 50 },
348
+ });
349
+ allEntries.push(...page.data);
350
+ cursor = page.hasMore ? page.nextCursor : undefined;
351
+ } while (cursor);
335
352
  ```
336
353
 
337
354
  ### `pauseAgent(agentId)`
@@ -481,6 +498,10 @@ interface ConnectionState {
481
498
  type HistoryFilters = {
482
499
  fromDate?: string; // YYYY-MM-DD
483
500
  toDate?: string; // YYYY-MM-DD
501
+ /** Max entries returned per call. Defaults to 10. */
502
+ limit?: number;
503
+ /** Opaque cursor returned by a previous getHistory call. */
504
+ cursor?: string;
484
505
  };
485
506
 
486
507
  type HistoryOptions = {
@@ -612,7 +633,10 @@ interface AgentHistoryEntry {
612
633
 
613
634
  interface OwneyAgentHistory {
614
635
  data: AgentHistoryEntry[];
615
- total: number;
636
+ /** True iff a subsequent page exists; pair with `nextCursor`. */
637
+ hasMore: boolean;
638
+ /** Opaque cursor for the next page. Present iff `hasMore === true`. */
639
+ nextCursor?: string;
616
640
  }
617
641
 
618
642
  interface OwneyAgentStatus {
@@ -750,6 +774,38 @@ The three convenience subclasses extend `OwneyError` and remain backwards-compat
750
774
  | `AgentNotFoundError` | `AGENT_NOT_FOUND` | — |
751
775
  | `AgentChainIncompatibleError` | `AGENT_CHAIN_INCOMPATIBLE` | `incompatibleAgents`, `connectedChainId` |
752
776
 
777
+ ### Cursor errors
778
+
779
+ `InvalidHistoryCursorError` is thrown by `getHistory` when the supplied cursor is invalid. It extends `Error` directly — **not** `OwneyError` — so a `catch` block that only checks `instanceof OwneyError` will not catch it. Catch it separately:
780
+
781
+ ```typescript
782
+ import { InvalidHistoryCursorError } from "@owney/sdk";
783
+
784
+ try {
785
+ const page = await sdk.getHistory({ filters: { cursor: storedCursor } });
786
+ } catch (error) {
787
+ if (error instanceof InvalidHistoryCursorError) {
788
+ // Stored cursor is stale or malformed — reset pagination.
789
+ storedCursor = undefined;
790
+ // Refetch first page...
791
+ } else {
792
+ throw error;
793
+ }
794
+ }
795
+ ```
796
+
797
+ `InvalidHistoryCursorError` carries a stable `code` property (value: `"invalid_cursor"`) and is thrown when:
798
+
799
+ - The supplied cursor is an empty string.
800
+ - The cursor is not valid base64 JSON.
801
+ - The cursor payload has the wrong shape (e.g. a cursor from a different pagination context).
802
+
803
+ **Recovery:** catch `InvalidHistoryCursorError`, clear your stored cursor, and refetch from page 1.
804
+
805
+ | Class | `code` | Extends |
806
+ | ---------------------------- | ----------------- | ---------- |
807
+ | `InvalidHistoryCursorError` | `"invalid_cursor"`| `Error` |
808
+
753
809
  ## License
754
810
 
755
811
  ELv2
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  AgentChainIncompatibleError: () => AgentChainIncompatibleError,
24
24
  AgentNotFoundError: () => AgentNotFoundError,
25
+ InvalidHistoryCursorError: () => InvalidHistoryCursorError,
25
26
  NotConnectedError: () => NotConnectedError,
26
27
  OwneyError: () => OwneyError,
27
28
  OwneySDK: () => OwneySDK
@@ -237,7 +238,7 @@ function mapApyHistory(raw) {
237
238
  apyByChainAndAsset: mapWeightedApyByChain(raw.weightedApyAfterFeeByChain)
238
239
  };
239
240
  }
240
- function mapHistory(raw, chainId) {
241
+ function mapEntries(rawEntries, chainId) {
241
242
  const actionMap = {
242
243
  "Deposit to protocol": "Deposit",
243
244
  "Top up wallet": "Top up",
@@ -245,50 +246,47 @@ function mapHistory(raw, chainId) {
245
246
  "Withdraw from protocol": "Withdraw",
246
247
  Rebalance: "Rebalance"
247
248
  };
248
- return {
249
- total: raw.total,
250
- data: raw.data.filter((d) => d.chainId === chainId).map((entry) => {
251
- const rawLog = entry.rebalanceLog;
252
- const positions = entry.positions ?? [];
253
- let rebalanceLog = [];
254
- let oldApy = null;
255
- let newApy = null;
256
- if (rawLog) {
257
- const matched = positions.find(
258
- (p) => p.protocol_name ? rawLog.newOpportunity.includes(`${p.protocol_name} (${p.pool})`) : false
259
- );
260
- oldApy = Number(rawLog.oldApy);
261
- newApy = Number(rawLog.newApy);
262
- rebalanceLog = [
263
- {
264
- fromProtocol: rawLog.oldOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
265
- toProtocol: rawLog.newOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
266
- tokenSymbol: matched?.token_symbol ?? "",
267
- amount: matched ? String(hexToDecimal(matched.amount || "0x")) : "",
268
- status: "success"
269
- }
270
- ];
271
- }
272
- const rawAmount = entry?.positions?.[0]?.amount;
273
- const hasAmount = typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
274
- return {
275
- agent: "zyfai",
276
- action: (entry.action ? actionMap[entry.action] : void 0) ?? entry.action,
277
- date: entry.date || "",
278
- newApy: String(newApy),
279
- oldApy: String(oldApy),
280
- transactions: [
281
- {
282
- txHashes: entry.transactionHash ? [entry.transactionHash] : [],
283
- chainId: entry.chainId,
284
- tokenSymbol: entry?.positions?.[0]?.token_symbol,
285
- amount: hasAmount ? String(hexToDecimal(rawAmount)) : void 0
286
- }
287
- ],
288
- rebalanceLog
289
- };
290
- })
291
- };
249
+ return rawEntries.map((entry) => {
250
+ const rawLog = entry.rebalanceLog;
251
+ const positions = entry.positions ?? [];
252
+ let rebalanceLog = [];
253
+ let oldApy = null;
254
+ let newApy = null;
255
+ if (rawLog) {
256
+ const matched = positions.find(
257
+ (p) => p.protocol_name ? rawLog.newOpportunity.includes(`${p.protocol_name} (${p.pool})`) : false
258
+ );
259
+ oldApy = Number(rawLog.oldApy);
260
+ newApy = Number(rawLog.newApy);
261
+ rebalanceLog = [
262
+ {
263
+ fromProtocol: rawLog.oldOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
264
+ toProtocol: rawLog.newOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
265
+ tokenSymbol: matched?.token_symbol ?? "",
266
+ amount: matched ? String(hexToDecimal(matched.amount || "0x")) : "",
267
+ status: "success"
268
+ }
269
+ ];
270
+ }
271
+ const rawAmount = entry?.positions?.[0]?.amount;
272
+ const hasAmount = typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
273
+ return {
274
+ agent: "zyfai",
275
+ action: (entry.action ? actionMap[entry.action] : void 0) ?? entry.action,
276
+ date: entry.date || "",
277
+ newApy: newApy === null ? null : String(newApy),
278
+ oldApy: oldApy === null ? null : String(oldApy),
279
+ transactions: [
280
+ {
281
+ txHashes: entry.transactionHash ? [entry.transactionHash] : [],
282
+ chainId: entry.chainId,
283
+ tokenSymbol: entry?.positions?.[0]?.token_symbol,
284
+ amount: hasAmount ? String(hexToDecimal(rawAmount)) : void 0
285
+ }
286
+ ],
287
+ rebalanceLog
288
+ };
289
+ });
292
290
  }
293
291
  function mapAgentStatus(raw) {
294
292
  return {
@@ -333,6 +331,33 @@ function mapApyByStrategy(raw) {
333
331
  };
334
332
  }
335
333
 
334
+ // src/agents/zyfai/zyfai.history-cursor.ts
335
+ var InvalidHistoryCursorError = class extends Error {
336
+ code = "invalid_cursor";
337
+ constructor(message) {
338
+ super(message);
339
+ this.name = "InvalidHistoryCursorError";
340
+ }
341
+ };
342
+ function encodeHistoryCursor(payload) {
343
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
344
+ }
345
+ function decodeHistoryCursor(token) {
346
+ if (!token)
347
+ throw new InvalidHistoryCursorError("Cursor is empty.");
348
+ let parsed;
349
+ try {
350
+ const json = Buffer.from(token, "base64").toString("utf8");
351
+ parsed = JSON.parse(json);
352
+ } catch {
353
+ throw new InvalidHistoryCursorError("Cursor is not valid base64 JSON.");
354
+ }
355
+ if (!parsed || typeof parsed !== "object" || typeof parsed.toDate !== "string" || parsed.toDate.length === 0 || Number.isNaN(Date.parse(parsed.toDate)) || typeof parsed.rawOffset !== "number" || !Number.isFinite(parsed.rawOffset) || parsed.rawOffset < 0 || !Number.isInteger(parsed.rawOffset)) {
356
+ throw new InvalidHistoryCursorError("Cursor payload has wrong shape.");
357
+ }
358
+ return parsed;
359
+ }
360
+
336
361
  // src/agents/zyfai/zyfai.auth-cache.ts
337
362
  var KEY_PREFIX = "owney.zyfai.session";
338
363
  var storage = () => {
@@ -647,8 +672,38 @@ var ZyfaiAgent = class {
647
672
  }
648
673
  async getHistory(state, chainId, options) {
649
674
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
650
- const raw = await this.sdk.getHistory(smartWallet, validChainId, options);
651
- return mapHistory(raw, validChainId);
675
+ const limit = options?.limit ?? 10;
676
+ const cursor = options?.cursor ? decodeHistoryCursor(options.cursor) : null;
677
+ const toDate = cursor?.toDate ?? options?.toDate ?? (/* @__PURE__ */ new Date()).toISOString();
678
+ let offset = cursor?.rawOffset ?? 0;
679
+ const MAX_BACKEND_CALLS = 5;
680
+ const MIN_BACKEND_PAGE_SIZE = 20;
681
+ const backendPageSize = Math.max(limit, MIN_BACKEND_PAGE_SIZE);
682
+ const matched = [];
683
+ let backendExhausted = false;
684
+ for (let i = 0; i < MAX_BACKEND_CALLS && matched.length < limit; i++) {
685
+ const raw = await this.sdk.getHistory(smartWallet, validChainId, {
686
+ limit: backendPageSize,
687
+ offset,
688
+ fromDate: options?.fromDate,
689
+ toDate
690
+ });
691
+ matched.push(...raw.data.filter((e) => e.chainId === validChainId));
692
+ offset += raw.data.length;
693
+ if (raw.data.length < backendPageSize) {
694
+ backendExhausted = true;
695
+ break;
696
+ }
697
+ }
698
+ const overshoot = Math.max(0, matched.length - limit);
699
+ const pageData = matched.slice(0, limit);
700
+ const hasMore = !backendExhausted || overshoot > 0;
701
+ const nextCursor = hasMore ? encodeHistoryCursor({ toDate, rawOffset: offset - overshoot }) : void 0;
702
+ return {
703
+ data: mapEntries(pageData, validChainId),
704
+ hasMore,
705
+ nextCursor
706
+ };
652
707
  }
653
708
  // --- IAgent: Agent control ---
654
709
  async pause(state, chainId) {
@@ -840,6 +895,26 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
840
895
  }
841
896
 
842
897
  // src/client.ts
898
+ function encodeMultiAgentCursor(map) {
899
+ return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
900
+ }
901
+ function decodeMultiAgentCursor(token) {
902
+ let parsed;
903
+ try {
904
+ const json = Buffer.from(token, "base64").toString("utf8");
905
+ parsed = JSON.parse(json);
906
+ } catch {
907
+ throw new InvalidHistoryCursorError(
908
+ "Multi-agent cursor is not valid base64 JSON."
909
+ );
910
+ }
911
+ if (!parsed || typeof parsed !== "object" || !Object.values(parsed).every((v) => typeof v === "string")) {
912
+ throw new InvalidHistoryCursorError(
913
+ "Multi-agent cursor payload has wrong shape."
914
+ );
915
+ }
916
+ return parsed;
917
+ }
843
918
  var OwneySDK = class {
844
919
  agents = /* @__PURE__ */ new Map();
845
920
  activeAgents = /* @__PURE__ */ new Set();
@@ -1509,10 +1584,20 @@ var OwneySDK = class {
1509
1584
  }
1510
1585
  /**
1511
1586
  * Get transaction history for a specific agent, or all agents.
1512
- * @param params - Optional. Contains agentId and filters.
1513
- * @param params.agentId - Optional. Agent to query. Omit for all agents.
1514
- * @param params.filters - Optional. Pagination and date filters (fromDate, toDate)
1515
- * @returns {OwneyAgentHistory} with per-agent history
1587
+ *
1588
+ * Pagination uses an opaque cursor (`filters.cursor`). The first call without
1589
+ * a cursor anchors a snapshot at "now"; subsequent pages within that snapshot
1590
+ * are stable. Each cursorless call captures a NEW snapshot, so do not mix
1591
+ * cursors issued by different first-page calls — refreshes that need to
1592
+ * preserve the user's scroll position should keep using cursors from the
1593
+ * original snapshot.
1594
+ *
1595
+ * @param params.agentId - Optional. Agent to query. Omit for all active agents.
1596
+ * @param params.filters - Optional. Pagination + date filters.
1597
+ * - `limit`: max entries per page (default 10)
1598
+ * - `cursor`: opaque cursor returned by a previous call
1599
+ * - `fromDate` / `toDate`: ISO 8601 date strings
1600
+ * @returns {OwneyAgentHistory} `data` (up to `limit` entries), `hasMore`, `nextCursor`.
1516
1601
  */
1517
1602
  async getHistory(params) {
1518
1603
  const state = this.requireState();
@@ -1521,21 +1606,40 @@ var OwneySDK = class {
1521
1606
  if (agentId) {
1522
1607
  return this.getAgent(agentId).getHistory(state, chainId, filters);
1523
1608
  }
1524
- const historyResults = await Promise.all(
1525
- [...this.getActiveAgents().values()].map(
1526
- (agent) => agent.getHistory(state, chainId, filters)
1527
- )
1609
+ const activeAgents = [...this.getActiveAgents().values()];
1610
+ const cursorMap = filters?.cursor ? decodeMultiAgentCursor(filters.cursor) : {};
1611
+ const perAgent = await Promise.all(
1612
+ activeAgents.map(async (agent) => {
1613
+ const agentCursor = cursorMap[agent.id];
1614
+ if (filters?.cursor && agentCursor === void 0) {
1615
+ return { agentId: agent.id, page: null };
1616
+ }
1617
+ const page = await agent.getHistory(state, chainId, {
1618
+ ...filters,
1619
+ cursor: agentCursor
1620
+ });
1621
+ return { agentId: agent.id, page };
1622
+ })
1528
1623
  );
1529
- let results = [];
1530
- for (const { data } of historyResults) {
1531
- results = [...results, ...data];
1624
+ const merged = [];
1625
+ const nextCursorMap = {};
1626
+ let hasMore = false;
1627
+ for (const { agentId: id, page } of perAgent) {
1628
+ if (!page)
1629
+ continue;
1630
+ merged.push(...page.data);
1631
+ if (page.hasMore && page.nextCursor) {
1632
+ nextCursorMap[id] = page.nextCursor;
1633
+ hasMore = true;
1634
+ }
1532
1635
  }
1533
- results.sort(
1636
+ merged.sort(
1534
1637
  (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
1535
1638
  );
1536
1639
  return {
1537
- total: results.length,
1538
- data: results
1640
+ data: merged,
1641
+ hasMore,
1642
+ nextCursor: hasMore ? encodeMultiAgentCursor(nextCursorMap) : void 0
1539
1643
  };
1540
1644
  }
1541
1645
  // --- Agent control ---
@@ -1690,6 +1794,7 @@ var OwneySDK = class {
1690
1794
  0 && (module.exports = {
1691
1795
  AgentChainIncompatibleError,
1692
1796
  AgentNotFoundError,
1797
+ InvalidHistoryCursorError,
1693
1798
  NotConnectedError,
1694
1799
  OwneyError,
1695
1800
  OwneySDK
package/dist/index.d.cts CHANGED
@@ -48,7 +48,7 @@ type AgentApyOptions = {
48
48
  chainId?: number;
49
49
  };
50
50
 
51
- type AgentId = "zyfai";
51
+ type AgentId = "zyfai" | "sail";
52
52
  type Asset = string;
53
53
  type AgentSupportedAsset = {
54
54
  readonly symbol: string;
@@ -63,6 +63,10 @@ type DailyApyDays = "7D" | "14D" | "30D";
63
63
  type HistoryFilters = {
64
64
  fromDate?: string;
65
65
  toDate?: string;
66
+ /** Max entries returned per call. Defaults to 10. */
67
+ limit?: number;
68
+ /** Opaque cursor returned by a previous getHistory call. */
69
+ cursor?: string;
66
70
  };
67
71
  type HistoryOptions = {
68
72
  agentId?: AgentId;
@@ -177,7 +181,10 @@ interface AgentHistoryEntry {
177
181
  }
178
182
  interface OwneyAgentHistory {
179
183
  data: AgentHistoryEntry[];
180
- total: number;
184
+ /** True iff a subsequent page exists; pair with `nextCursor`. */
185
+ hasMore: boolean;
186
+ /** Opaque cursor for the next page. Present iff `hasMore === true`. */
187
+ nextCursor?: string;
181
188
  }
182
189
  interface OwneyAgentStatus {
183
190
  success: boolean;
@@ -320,10 +327,20 @@ declare class OwneySDK {
320
327
  getAccountApy({ agentId, days, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
321
328
  /**
322
329
  * Get transaction history for a specific agent, or all agents.
323
- * @param params - Optional. Contains agentId and filters.
324
- * @param params.agentId - Optional. Agent to query. Omit for all agents.
325
- * @param params.filters - Optional. Pagination and date filters (fromDate, toDate)
326
- * @returns {OwneyAgentHistory} with per-agent history
330
+ *
331
+ * Pagination uses an opaque cursor (`filters.cursor`). The first call without
332
+ * a cursor anchors a snapshot at "now"; subsequent pages within that snapshot
333
+ * are stable. Each cursorless call captures a NEW snapshot, so do not mix
334
+ * cursors issued by different first-page calls — refreshes that need to
335
+ * preserve the user's scroll position should keep using cursors from the
336
+ * original snapshot.
337
+ *
338
+ * @param params.agentId - Optional. Agent to query. Omit for all active agents.
339
+ * @param params.filters - Optional. Pagination + date filters.
340
+ * - `limit`: max entries per page (default 10)
341
+ * - `cursor`: opaque cursor returned by a previous call
342
+ * - `fromDate` / `toDate`: ISO 8601 date strings
343
+ * @returns {OwneyAgentHistory} `data` (up to `limit` entries), `hasMore`, `nextCursor`.
327
344
  */
328
345
  getHistory(params?: HistoryOptions): Promise<OwneyAgentHistory>;
329
346
  /**
@@ -389,4 +406,9 @@ declare class AgentChainIncompatibleError extends OwneyError {
389
406
  }>, connectedChainId: number);
390
407
  }
391
408
 
392
- export { AccountAgentApy, AccountApyOptions, AgentApy, AgentApyDetails, AgentBalance, AgentChainIncompatibleError, AgentEarnings, AgentHistoryEntry, AgentId, AgentNotFoundError, AgentSupportedAsset, AgentSupportedAssets, AgentUserProfile, AgentWithdrawResult, AgentsApyOptions, ApyByChainAndAsset, 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 };
409
+ declare class InvalidHistoryCursorError extends Error {
410
+ readonly code: "invalid_cursor";
411
+ constructor(message: string);
412
+ }
413
+
414
+ export { AccountAgentApy, AccountApyOptions, AgentApy, AgentApyDetails, AgentBalance, AgentChainIncompatibleError, AgentEarnings, AgentHistoryEntry, AgentId, AgentNotFoundError, AgentSupportedAsset, AgentSupportedAssets, AgentUserProfile, AgentWithdrawResult, AgentsApyOptions, ApyByChainAndAsset, Asset, ConnectionState, DailyApyDays, DepositCallback, DepositOptions, HistoryAction, HistoryFilters, HistoryOptions, HistoryTransaction, IAgent, InvalidHistoryCursorError, 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.d.ts CHANGED
@@ -48,7 +48,7 @@ type AgentApyOptions = {
48
48
  chainId?: number;
49
49
  };
50
50
 
51
- type AgentId = "zyfai";
51
+ type AgentId = "zyfai" | "sail";
52
52
  type Asset = string;
53
53
  type AgentSupportedAsset = {
54
54
  readonly symbol: string;
@@ -63,6 +63,10 @@ type DailyApyDays = "7D" | "14D" | "30D";
63
63
  type HistoryFilters = {
64
64
  fromDate?: string;
65
65
  toDate?: string;
66
+ /** Max entries returned per call. Defaults to 10. */
67
+ limit?: number;
68
+ /** Opaque cursor returned by a previous getHistory call. */
69
+ cursor?: string;
66
70
  };
67
71
  type HistoryOptions = {
68
72
  agentId?: AgentId;
@@ -177,7 +181,10 @@ interface AgentHistoryEntry {
177
181
  }
178
182
  interface OwneyAgentHistory {
179
183
  data: AgentHistoryEntry[];
180
- total: number;
184
+ /** True iff a subsequent page exists; pair with `nextCursor`. */
185
+ hasMore: boolean;
186
+ /** Opaque cursor for the next page. Present iff `hasMore === true`. */
187
+ nextCursor?: string;
181
188
  }
182
189
  interface OwneyAgentStatus {
183
190
  success: boolean;
@@ -320,10 +327,20 @@ declare class OwneySDK {
320
327
  getAccountApy({ agentId, days, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
321
328
  /**
322
329
  * Get transaction history for a specific agent, or all agents.
323
- * @param params - Optional. Contains agentId and filters.
324
- * @param params.agentId - Optional. Agent to query. Omit for all agents.
325
- * @param params.filters - Optional. Pagination and date filters (fromDate, toDate)
326
- * @returns {OwneyAgentHistory} with per-agent history
330
+ *
331
+ * Pagination uses an opaque cursor (`filters.cursor`). The first call without
332
+ * a cursor anchors a snapshot at "now"; subsequent pages within that snapshot
333
+ * are stable. Each cursorless call captures a NEW snapshot, so do not mix
334
+ * cursors issued by different first-page calls — refreshes that need to
335
+ * preserve the user's scroll position should keep using cursors from the
336
+ * original snapshot.
337
+ *
338
+ * @param params.agentId - Optional. Agent to query. Omit for all active agents.
339
+ * @param params.filters - Optional. Pagination + date filters.
340
+ * - `limit`: max entries per page (default 10)
341
+ * - `cursor`: opaque cursor returned by a previous call
342
+ * - `fromDate` / `toDate`: ISO 8601 date strings
343
+ * @returns {OwneyAgentHistory} `data` (up to `limit` entries), `hasMore`, `nextCursor`.
327
344
  */
328
345
  getHistory(params?: HistoryOptions): Promise<OwneyAgentHistory>;
329
346
  /**
@@ -389,4 +406,9 @@ declare class AgentChainIncompatibleError extends OwneyError {
389
406
  }>, connectedChainId: number);
390
407
  }
391
408
 
392
- export { AccountAgentApy, AccountApyOptions, AgentApy, AgentApyDetails, AgentBalance, AgentChainIncompatibleError, AgentEarnings, AgentHistoryEntry, AgentId, AgentNotFoundError, AgentSupportedAsset, AgentSupportedAssets, AgentUserProfile, AgentWithdrawResult, AgentsApyOptions, ApyByChainAndAsset, 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 };
409
+ declare class InvalidHistoryCursorError extends Error {
410
+ readonly code: "invalid_cursor";
411
+ constructor(message: string);
412
+ }
413
+
414
+ export { AccountAgentApy, AccountApyOptions, AgentApy, AgentApyDetails, AgentBalance, AgentChainIncompatibleError, AgentEarnings, AgentHistoryEntry, AgentId, AgentNotFoundError, AgentSupportedAsset, AgentSupportedAssets, AgentUserProfile, AgentWithdrawResult, AgentsApyOptions, ApyByChainAndAsset, Asset, ConnectionState, DailyApyDays, DepositCallback, DepositOptions, HistoryAction, HistoryFilters, HistoryOptions, HistoryTransaction, IAgent, InvalidHistoryCursorError, 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
@@ -207,7 +207,7 @@ function mapApyHistory(raw) {
207
207
  apyByChainAndAsset: mapWeightedApyByChain(raw.weightedApyAfterFeeByChain)
208
208
  };
209
209
  }
210
- function mapHistory(raw, chainId) {
210
+ function mapEntries(rawEntries, chainId) {
211
211
  const actionMap = {
212
212
  "Deposit to protocol": "Deposit",
213
213
  "Top up wallet": "Top up",
@@ -215,50 +215,47 @@ function mapHistory(raw, chainId) {
215
215
  "Withdraw from protocol": "Withdraw",
216
216
  Rebalance: "Rebalance"
217
217
  };
218
- return {
219
- total: raw.total,
220
- data: raw.data.filter((d) => d.chainId === chainId).map((entry) => {
221
- const rawLog = entry.rebalanceLog;
222
- const positions = entry.positions ?? [];
223
- let rebalanceLog = [];
224
- let oldApy = null;
225
- let newApy = null;
226
- if (rawLog) {
227
- const matched = positions.find(
228
- (p) => p.protocol_name ? rawLog.newOpportunity.includes(`${p.protocol_name} (${p.pool})`) : false
229
- );
230
- oldApy = Number(rawLog.oldApy);
231
- newApy = Number(rawLog.newApy);
232
- rebalanceLog = [
233
- {
234
- fromProtocol: rawLog.oldOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
235
- toProtocol: rawLog.newOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
236
- tokenSymbol: matched?.token_symbol ?? "",
237
- amount: matched ? String(hexToDecimal(matched.amount || "0x")) : "",
238
- status: "success"
239
- }
240
- ];
241
- }
242
- const rawAmount = entry?.positions?.[0]?.amount;
243
- const hasAmount = typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
244
- return {
245
- agent: "zyfai",
246
- action: (entry.action ? actionMap[entry.action] : void 0) ?? entry.action,
247
- date: entry.date || "",
248
- newApy: String(newApy),
249
- oldApy: String(oldApy),
250
- transactions: [
251
- {
252
- txHashes: entry.transactionHash ? [entry.transactionHash] : [],
253
- chainId: entry.chainId,
254
- tokenSymbol: entry?.positions?.[0]?.token_symbol,
255
- amount: hasAmount ? String(hexToDecimal(rawAmount)) : void 0
256
- }
257
- ],
258
- rebalanceLog
259
- };
260
- })
261
- };
218
+ return rawEntries.map((entry) => {
219
+ const rawLog = entry.rebalanceLog;
220
+ const positions = entry.positions ?? [];
221
+ let rebalanceLog = [];
222
+ let oldApy = null;
223
+ let newApy = null;
224
+ if (rawLog) {
225
+ const matched = positions.find(
226
+ (p) => p.protocol_name ? rawLog.newOpportunity.includes(`${p.protocol_name} (${p.pool})`) : false
227
+ );
228
+ oldApy = Number(rawLog.oldApy);
229
+ newApy = Number(rawLog.newApy);
230
+ rebalanceLog = [
231
+ {
232
+ fromProtocol: rawLog.oldOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
233
+ toProtocol: rawLog.newOpportunity.replace(/\s*\(.*?\)/g, "").trim(),
234
+ tokenSymbol: matched?.token_symbol ?? "",
235
+ amount: matched ? String(hexToDecimal(matched.amount || "0x")) : "",
236
+ status: "success"
237
+ }
238
+ ];
239
+ }
240
+ const rawAmount = entry?.positions?.[0]?.amount;
241
+ const hasAmount = typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
242
+ return {
243
+ agent: "zyfai",
244
+ action: (entry.action ? actionMap[entry.action] : void 0) ?? entry.action,
245
+ date: entry.date || "",
246
+ newApy: newApy === null ? null : String(newApy),
247
+ oldApy: oldApy === null ? null : String(oldApy),
248
+ transactions: [
249
+ {
250
+ txHashes: entry.transactionHash ? [entry.transactionHash] : [],
251
+ chainId: entry.chainId,
252
+ tokenSymbol: entry?.positions?.[0]?.token_symbol,
253
+ amount: hasAmount ? String(hexToDecimal(rawAmount)) : void 0
254
+ }
255
+ ],
256
+ rebalanceLog
257
+ };
258
+ });
262
259
  }
263
260
  function mapAgentStatus(raw) {
264
261
  return {
@@ -303,6 +300,33 @@ function mapApyByStrategy(raw) {
303
300
  };
304
301
  }
305
302
 
303
+ // src/agents/zyfai/zyfai.history-cursor.ts
304
+ var InvalidHistoryCursorError = class extends Error {
305
+ code = "invalid_cursor";
306
+ constructor(message) {
307
+ super(message);
308
+ this.name = "InvalidHistoryCursorError";
309
+ }
310
+ };
311
+ function encodeHistoryCursor(payload) {
312
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
313
+ }
314
+ function decodeHistoryCursor(token) {
315
+ if (!token)
316
+ throw new InvalidHistoryCursorError("Cursor is empty.");
317
+ let parsed;
318
+ try {
319
+ const json = Buffer.from(token, "base64").toString("utf8");
320
+ parsed = JSON.parse(json);
321
+ } catch {
322
+ throw new InvalidHistoryCursorError("Cursor is not valid base64 JSON.");
323
+ }
324
+ if (!parsed || typeof parsed !== "object" || typeof parsed.toDate !== "string" || parsed.toDate.length === 0 || Number.isNaN(Date.parse(parsed.toDate)) || typeof parsed.rawOffset !== "number" || !Number.isFinite(parsed.rawOffset) || parsed.rawOffset < 0 || !Number.isInteger(parsed.rawOffset)) {
325
+ throw new InvalidHistoryCursorError("Cursor payload has wrong shape.");
326
+ }
327
+ return parsed;
328
+ }
329
+
306
330
  // src/agents/zyfai/zyfai.auth-cache.ts
307
331
  var KEY_PREFIX = "owney.zyfai.session";
308
332
  var storage = () => {
@@ -617,8 +641,38 @@ var ZyfaiAgent = class {
617
641
  }
618
642
  async getHistory(state, chainId, options) {
619
643
  const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
620
- const raw = await this.sdk.getHistory(smartWallet, validChainId, options);
621
- return mapHistory(raw, validChainId);
644
+ const limit = options?.limit ?? 10;
645
+ const cursor = options?.cursor ? decodeHistoryCursor(options.cursor) : null;
646
+ const toDate = cursor?.toDate ?? options?.toDate ?? (/* @__PURE__ */ new Date()).toISOString();
647
+ let offset = cursor?.rawOffset ?? 0;
648
+ const MAX_BACKEND_CALLS = 5;
649
+ const MIN_BACKEND_PAGE_SIZE = 20;
650
+ const backendPageSize = Math.max(limit, MIN_BACKEND_PAGE_SIZE);
651
+ const matched = [];
652
+ let backendExhausted = false;
653
+ for (let i = 0; i < MAX_BACKEND_CALLS && matched.length < limit; i++) {
654
+ const raw = await this.sdk.getHistory(smartWallet, validChainId, {
655
+ limit: backendPageSize,
656
+ offset,
657
+ fromDate: options?.fromDate,
658
+ toDate
659
+ });
660
+ matched.push(...raw.data.filter((e) => e.chainId === validChainId));
661
+ offset += raw.data.length;
662
+ if (raw.data.length < backendPageSize) {
663
+ backendExhausted = true;
664
+ break;
665
+ }
666
+ }
667
+ const overshoot = Math.max(0, matched.length - limit);
668
+ const pageData = matched.slice(0, limit);
669
+ const hasMore = !backendExhausted || overshoot > 0;
670
+ const nextCursor = hasMore ? encodeHistoryCursor({ toDate, rawOffset: offset - overshoot }) : void 0;
671
+ return {
672
+ data: mapEntries(pageData, validChainId),
673
+ hasMore,
674
+ nextCursor
675
+ };
622
676
  }
623
677
  // --- IAgent: Agent control ---
624
678
  async pause(state, chainId) {
@@ -810,6 +864,26 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
810
864
  }
811
865
 
812
866
  // src/client.ts
867
+ function encodeMultiAgentCursor(map) {
868
+ return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
869
+ }
870
+ function decodeMultiAgentCursor(token) {
871
+ let parsed;
872
+ try {
873
+ const json = Buffer.from(token, "base64").toString("utf8");
874
+ parsed = JSON.parse(json);
875
+ } catch {
876
+ throw new InvalidHistoryCursorError(
877
+ "Multi-agent cursor is not valid base64 JSON."
878
+ );
879
+ }
880
+ if (!parsed || typeof parsed !== "object" || !Object.values(parsed).every((v) => typeof v === "string")) {
881
+ throw new InvalidHistoryCursorError(
882
+ "Multi-agent cursor payload has wrong shape."
883
+ );
884
+ }
885
+ return parsed;
886
+ }
813
887
  var OwneySDK = class {
814
888
  agents = /* @__PURE__ */ new Map();
815
889
  activeAgents = /* @__PURE__ */ new Set();
@@ -1479,10 +1553,20 @@ var OwneySDK = class {
1479
1553
  }
1480
1554
  /**
1481
1555
  * Get transaction history for a specific agent, or all agents.
1482
- * @param params - Optional. Contains agentId and filters.
1483
- * @param params.agentId - Optional. Agent to query. Omit for all agents.
1484
- * @param params.filters - Optional. Pagination and date filters (fromDate, toDate)
1485
- * @returns {OwneyAgentHistory} with per-agent history
1556
+ *
1557
+ * Pagination uses an opaque cursor (`filters.cursor`). The first call without
1558
+ * a cursor anchors a snapshot at "now"; subsequent pages within that snapshot
1559
+ * are stable. Each cursorless call captures a NEW snapshot, so do not mix
1560
+ * cursors issued by different first-page calls — refreshes that need to
1561
+ * preserve the user's scroll position should keep using cursors from the
1562
+ * original snapshot.
1563
+ *
1564
+ * @param params.agentId - Optional. Agent to query. Omit for all active agents.
1565
+ * @param params.filters - Optional. Pagination + date filters.
1566
+ * - `limit`: max entries per page (default 10)
1567
+ * - `cursor`: opaque cursor returned by a previous call
1568
+ * - `fromDate` / `toDate`: ISO 8601 date strings
1569
+ * @returns {OwneyAgentHistory} `data` (up to `limit` entries), `hasMore`, `nextCursor`.
1486
1570
  */
1487
1571
  async getHistory(params) {
1488
1572
  const state = this.requireState();
@@ -1491,21 +1575,40 @@ var OwneySDK = class {
1491
1575
  if (agentId) {
1492
1576
  return this.getAgent(agentId).getHistory(state, chainId, filters);
1493
1577
  }
1494
- const historyResults = await Promise.all(
1495
- [...this.getActiveAgents().values()].map(
1496
- (agent) => agent.getHistory(state, chainId, filters)
1497
- )
1578
+ const activeAgents = [...this.getActiveAgents().values()];
1579
+ const cursorMap = filters?.cursor ? decodeMultiAgentCursor(filters.cursor) : {};
1580
+ const perAgent = await Promise.all(
1581
+ activeAgents.map(async (agent) => {
1582
+ const agentCursor = cursorMap[agent.id];
1583
+ if (filters?.cursor && agentCursor === void 0) {
1584
+ return { agentId: agent.id, page: null };
1585
+ }
1586
+ const page = await agent.getHistory(state, chainId, {
1587
+ ...filters,
1588
+ cursor: agentCursor
1589
+ });
1590
+ return { agentId: agent.id, page };
1591
+ })
1498
1592
  );
1499
- let results = [];
1500
- for (const { data } of historyResults) {
1501
- results = [...results, ...data];
1593
+ const merged = [];
1594
+ const nextCursorMap = {};
1595
+ let hasMore = false;
1596
+ for (const { agentId: id, page } of perAgent) {
1597
+ if (!page)
1598
+ continue;
1599
+ merged.push(...page.data);
1600
+ if (page.hasMore && page.nextCursor) {
1601
+ nextCursorMap[id] = page.nextCursor;
1602
+ hasMore = true;
1603
+ }
1502
1604
  }
1503
- results.sort(
1605
+ merged.sort(
1504
1606
  (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
1505
1607
  );
1506
1608
  return {
1507
- total: results.length,
1508
- data: results
1609
+ data: merged,
1610
+ hasMore,
1611
+ nextCursor: hasMore ? encodeMultiAgentCursor(nextCursorMap) : void 0
1509
1612
  };
1510
1613
  }
1511
1614
  // --- Agent control ---
@@ -1659,6 +1762,7 @@ var OwneySDK = class {
1659
1762
  export {
1660
1763
  AgentChainIncompatibleError,
1661
1764
  AgentNotFoundError,
1765
+ InvalidHistoryCursorError,
1662
1766
  NotConnectedError,
1663
1767
  OwneyError,
1664
1768
  OwneySDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",