@logswap/sdk 0.5.75 → 0.5.76

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/views.js ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * views.ts — the indexer's derived views (indexing phase 2, logswap-be `devnet/VIEWS.md`).
3
+ *
4
+ * The indexer records events and the snapshotter samples state; these views join the two in
5
+ * Postgres: holders, a wallet's positions and flows, per-swap volume and fee, price points and
6
+ * candles, TVL by the hour, the pools table's 24 h / 7 d figures, the generation steps and pad
7
+ * status, protocol revenue by day, and who did each routed action. Each reader returns typed rows:
8
+ *
9
+ * - **amounts** (Postgres `numeric`, GraphQL BigFloat strings) as `bigint`. The views floor every
10
+ * amount they compute, so the integer part is the value;
11
+ * - **prices, APYs, progress** (`double precision`) as `number`. `priceRaw` is quote wei per base
12
+ * wei: scale by 10^(baseDecimals − quoteDecimals) to display;
13
+ * - **times** as unix seconds (`number`), **addresses** checksummed, pool ids lowercase `0x…`.
14
+ *
15
+ * A view is a cache like any indexed table: the readers throw when the indexer cannot answer, and
16
+ * the SDK functions that use them ({@link holdingsOf}, {@link swapFees}, {@link fPoolShareHolders})
17
+ * keep their node path as the fallback.
18
+ */
19
+ import { getAddress } from "viem";
20
+ import { gql } from "./indexer.js";
21
+ const PAGE = 1000; // postgraphile's cap
22
+ const MAX_PAGES = 200;
23
+ const PARALLEL_PAGES = 8;
24
+ /** Every row of a view, paged by offset (a view has no primary key to page a cursor on). */
25
+ async function viewRows(url, field, type, selection, q) {
26
+ for (const o of q.orderBy ?? [])
27
+ if (!/^[A-Z0-9_]+$/.test(o))
28
+ throw new Error(`views: orderBy ${JSON.stringify(o)} is not an enum name`);
29
+ const hasCondition = q.condition !== undefined && Object.keys(q.condition).length > 0;
30
+ const hasFilter = q.filter !== undefined && Object.keys(q.filter).length > 0;
31
+ const pageSize = Math.min(q.first ?? PAGE, PAGE);
32
+ // postgraphile refuses a null or empty condition/filter: each appears only when it has content
33
+ const decl = `$offset: Int${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
34
+ const args = [
35
+ `first: ${pageSize}`,
36
+ "offset: $offset",
37
+ q.orderBy?.length ? `orderBy: [${q.orderBy.join(", ")}]` : "",
38
+ hasCondition ? "condition: $condition" : "",
39
+ hasFilter ? "filter: $filter" : "",
40
+ ].filter(Boolean).join(", ");
41
+ const query = `query View(${decl}) { ${field}(${args}) { totalCount nodes { ${selection} } } }`;
42
+ const page = async (offset) => (await gql(url, query, { offset, ...(hasCondition ? { condition: q.condition } : {}), ...(hasFilter ? { filter: q.filter } : {}) }, q.signal))[field];
43
+ const first = await page(0);
44
+ const rows = [...first.nodes];
45
+ if (q.first === undefined && first.totalCount > first.nodes.length) {
46
+ const pages = Math.ceil(first.totalCount / PAGE);
47
+ if (pages > MAX_PAGES)
48
+ throw new Error(`views: ${field} has more than ${PAGE * MAX_PAGES} rows; narrow the query`);
49
+ const offsets = Array.from({ length: pages - 1 }, (_, i) => (i + 1) * PAGE);
50
+ for (let i = 0; i < offsets.length; i += PARALLEL_PAGES) {
51
+ for (const g of await Promise.all(offsets.slice(i, i + PARALLEL_PAGES).map(page)))
52
+ rows.push(...g.nodes);
53
+ }
54
+ }
55
+ else if (q.first !== undefined && q.first > PAGE) {
56
+ // a cap above one page: the next pages up to it
57
+ for (let offset = PAGE; offset < Math.min(q.first, first.totalCount); offset += PAGE)
58
+ rows.push(...(await page(offset)).nodes);
59
+ rows.length = Math.min(rows.length, q.first);
60
+ }
61
+ return rows;
62
+ }
63
+ // ─── column parsers ──────────────────────────────────────────────────────────
64
+ /** A numeric / bigint column (BigFloat or BigInt string, or a number) → bigint; the integer part. */
65
+ function big(v) {
66
+ if (typeof v === "bigint")
67
+ return v;
68
+ if (typeof v === "number")
69
+ return BigInt(Math.trunc(v));
70
+ if (typeof v !== "string" || v === "")
71
+ throw new Error(`views: not a number: ${JSON.stringify(v)}`);
72
+ return BigInt(v.split(".")[0]);
73
+ }
74
+ const bigOr = (v) => (v === null || v === undefined ? null : big(v));
75
+ const num = (v) => Number(v);
76
+ const numOr = (v) => (v === null || v === undefined ? null : Number(v));
77
+ const intOr = (v) => (v === null || v === undefined ? null : Number(v));
78
+ const addr = (v) => getAddress(String(v));
79
+ const addrOr = (v) => (v === null || v === undefined ? null : getAddress(String(v)));
80
+ const secs = (v) => Date.parse(String(v)) / 1000;
81
+ const secsOr = (v) => (v === null || v === undefined ? null : secs(v));
82
+ const hex = (v) => String(v).toLowerCase();
83
+ const addrs = (v) => (v ?? []).map(addr);
84
+ export async function poolDirectory(url, q = {}) {
85
+ const rows = await viewRows(url, "allPoolDirectories", "PoolDirectory", "side poolId manager kind quote bases swapFee tickSpacing policy creator birthBlock birthTime shareKey", q);
86
+ return rows.map((r) => ({
87
+ side: r.side, poolId: hex(r.poolId), manager: addr(r.manager), kind: String(r.kind), quote: addr(r.quote), bases: addrs(r.bases),
88
+ swapFee: big(r.swapFee), tickSpacing: bigOr(r.tickSpacing), policy: addrOr(r.policy), creator: addrOr(r.creator),
89
+ birthBlock: big(r.birthBlock), birthTime: secs(r.birthTime), shareKey: bigOr(r.shareKey),
90
+ }));
91
+ }
92
+ export async function eventUsers(url, q = {}) {
93
+ const rows = await viewRows(url, "allEventUsers", "EventUser", "side event poolId actor viaRouter user txHash logIndex blockNumber blockTimestamp manager", q);
94
+ return rows.map((r) => ({
95
+ side: r.side, event: String(r.event), poolId: hex(r.poolId), actor: addr(r.actor), viaRouter: Boolean(r.viaRouter), user: addr(r.user),
96
+ txHash: hex(r.txHash), logIndex: num(r.logIndex), blockNumber: big(r.blockNumber), blockTimestamp: secs(r.blockTimestamp), manager: addr(r.manager),
97
+ }));
98
+ }
99
+ export async function poolHolders(url, q = {}) {
100
+ const rows = await viewRows(url, "allPoolHolders", "PoolHolder", "side manager id poolId gen floor cap capped holder balance isBackstop isDead", q);
101
+ return rows.map((r) => ({
102
+ side: r.side, manager: addr(r.manager), id: big(r.id), poolId: r.poolId == null ? null : hex(r.poolId), gen: intOr(r.gen),
103
+ floor: bigOr(r.floor), cap: bigOr(r.cap), capped: r.capped == null ? null : Boolean(r.capped),
104
+ holder: addr(r.holder), balance: big(r.balance), isBackstop: Boolean(r.isBackstop), isDead: Boolean(r.isDead),
105
+ }));
106
+ }
107
+ export async function walletPositions(url, q = {}) {
108
+ const rows = await viewRows(url, "allWalletPositions", "WalletPosition", "side manager wallet poolId kind quote bases id gen floor cap capped shares valueQuote status valuedAtBlock", q);
109
+ return rows.map((r) => ({
110
+ side: r.side, manager: addr(r.manager), wallet: addr(r.wallet), poolId: r.poolId == null ? null : hex(r.poolId), kind: r.kind == null ? null : String(r.kind),
111
+ quote: addrOr(r.quote), bases: addrs(r.bases), id: big(r.id), gen: intOr(r.gen), floor: bigOr(r.floor), cap: bigOr(r.cap),
112
+ capped: r.capped == null ? null : Boolean(r.capped), shares: big(r.shares), valueQuote: bigOr(r.valueQuote), status: String(r.status), valuedAtBlock: bigOr(r.valuedAtBlock),
113
+ }));
114
+ }
115
+ export async function walletPoolFlows(url, q = {}) {
116
+ const rows = await viewRows(url, "allWalletPoolFlows", "WalletPoolFlow", "side manager wallet poolId baseIn quoteIn baseOut quoteOut feesRealised dividendsClaimed redeemedQuote entries exits firstTime lastTime", q);
117
+ return rows.map((r) => ({
118
+ side: r.side, manager: addr(r.manager), wallet: addr(r.wallet), poolId: hex(r.poolId),
119
+ baseIn: big(r.baseIn), quoteIn: big(r.quoteIn), baseOut: big(r.baseOut), quoteOut: big(r.quoteOut),
120
+ feesRealised: big(r.feesRealised), dividendsClaimed: big(r.dividendsClaimed), redeemedQuote: big(r.redeemedQuote),
121
+ entries: num(r.entries), exits: num(r.exits), firstTime: secs(r.firstTime), lastTime: secs(r.lastTime),
122
+ }));
123
+ }
124
+ export async function swapFlows(url, q = {}) {
125
+ const rows = await viewRows(url, "allSwapFlows", "SwapFlow", "side manager poolId txHash logIndex blockNumber blockTimestamp volumeQuote feeQuote tokenIn tokenOut reserveIn reserveOut lIn lOut", q);
126
+ return rows.map((r) => ({
127
+ side: r.side, manager: addr(r.manager), poolId: hex(r.poolId), txHash: hex(r.txHash), logIndex: num(r.logIndex),
128
+ blockNumber: big(r.blockNumber), blockTimestamp: secs(r.blockTimestamp), volumeQuote: big(r.volumeQuote), feeQuote: big(r.feeQuote),
129
+ tokenIn: addrOr(r.tokenIn), tokenOut: addrOr(r.tokenOut), reserveIn: bigOr(r.reserveIn), reserveOut: bigOr(r.reserveOut), lIn: bigOr(r.lIn), lOut: bigOr(r.lOut),
130
+ }));
131
+ }
132
+ export async function priceTicks(url, q = {}) {
133
+ const rows = await viewRows(url, "allPriceTicks", "PriceTick", "side poolId base blockNumber blockTimestamp logIndex priceRaw volumeQuote feeQuote", q);
134
+ return rows.map((r) => ({
135
+ side: r.side, poolId: hex(r.poolId), base: addr(r.base), blockNumber: big(r.blockNumber), blockTimestamp: secs(r.blockTimestamp),
136
+ logIndex: num(r.logIndex), priceRaw: num(r.priceRaw), volumeQuote: big(r.volumeQuote), feeQuote: big(r.feeQuote),
137
+ }));
138
+ }
139
+ const CANDLE = { minute: ["allCandleMinutes", "CandleMinute"], hour: ["allCandleHours", "CandleHour"], day: ["allCandleDays", "CandleDay"] };
140
+ export async function candles(url, interval, q = {}) {
141
+ const [field, type] = CANDLE[interval];
142
+ const rows = await viewRows(url, field, type, "side poolId base bucket open high low close volumeQuote feeQuote swaps", q);
143
+ return rows.map((r) => ({
144
+ side: r.side, poolId: hex(r.poolId), base: addr(r.base), bucket: secs(r.bucket), open: num(r.open), high: num(r.high), low: num(r.low), close: num(r.close),
145
+ volumeQuote: big(r.volumeQuote), feeQuote: big(r.feeQuote), swaps: num(r.swaps),
146
+ }));
147
+ }
148
+ export async function poolTvlHours(url, q = {}) {
149
+ const rows = await viewRows(url, "allPoolTvlHours", "PoolTvlHour", "side poolId bucket blockNumber tvlQuote liquidity priceRaw feeGrowth shareValue gen", q);
150
+ return rows.map((r) => ({
151
+ side: r.side, poolId: hex(r.poolId), bucket: secs(r.bucket), blockNumber: big(r.blockNumber), tvlQuote: bigOr(r.tvlQuote),
152
+ liquidity: bigOr(r.liquidity), priceRaw: numOr(r.priceRaw), feeGrowth: bigOr(r.feeGrowth), shareValue: bigOr(r.shareValue), gen: intOr(r.gen),
153
+ }));
154
+ }
155
+ export async function poolSummaries(url, q = {}) {
156
+ const rows = await viewRows(url, "allPoolSummaries", "PoolSummary", "side poolId kind quote bases swapFee birthTime volume24H fees24H swaps24H volume7D fees7D volumeAll feesAll lastSwap tvlQuote liquidity priceRaw shareValue gen stateBlock seconds24H seconds7D apy24H apy7D", q);
157
+ return rows.map((r) => ({
158
+ side: r.side, poolId: hex(r.poolId), kind: String(r.kind), quote: addr(r.quote), bases: addrs(r.bases), swapFee: big(r.swapFee), birthTime: secs(r.birthTime),
159
+ volume24h: big(r.volume24H), fees24h: big(r.fees24H), swaps24h: num(r.swaps24H), volume7d: big(r.volume7D), fees7d: big(r.fees7D),
160
+ volumeAll: big(r.volumeAll), feesAll: big(r.feesAll), lastSwap: secsOr(r.lastSwap), tvlQuote: bigOr(r.tvlQuote), liquidity: bigOr(r.liquidity),
161
+ priceRaw: numOr(r.priceRaw), shareValue: bigOr(r.shareValue), gen: intOr(r.gen), stateBlock: bigOr(r.stateBlock),
162
+ seconds24h: num(r.seconds24H), seconds7d: num(r.seconds7D), apy24h: numOr(r.apy24H), apy7d: numOr(r.apy7D),
163
+ }));
164
+ }
165
+ export async function generationSteps(url, q = {}) {
166
+ const rows = await viewRows(url, "allGenerationSteps", "GenerationStep", "kind poolId genAfter pot payee toPayee cut burnt deepened rung txHash blockNumber blockTimestamp", q);
167
+ return rows.map((r) => ({
168
+ kind: r.kind == null ? null : String(r.kind), poolId: hex(r.poolId), genAfter: num(r.genAfter), pot: big(r.pot), payee: addr(r.payee),
169
+ toPayee: big(r.toPayee), cut: big(r.cut), burnt: big(r.burnt), deepened: big(r.deepened), rung: intOr(r.rung),
170
+ txHash: hex(r.txHash), blockNumber: big(r.blockNumber), blockTimestamp: secs(r.blockTimestamp),
171
+ }));
172
+ }
173
+ export async function padStatuses(url, q = {}) {
174
+ const rows = await viewRows(url, "allPadStatuses", "PadStatus", "kind poolId quote bases creator birthTime gen pot unharvested l q tvlQuote shareValue threshold progress stateBlock stateTime steps burntTotal cutTotal paidTotal deepenedTotal lastStep", q);
175
+ return rows.map((r) => ({
176
+ kind: String(r.kind), poolId: hex(r.poolId), quote: addr(r.quote), bases: addrs(r.bases), creator: addrOr(r.creator), birthTime: secs(r.birthTime),
177
+ gen: intOr(r.gen), pot: bigOr(r.pot), unharvested: bigOr(r.unharvested), l: bigOr(r.l), q: bigOr(r.q), tvlQuote: bigOr(r.tvlQuote),
178
+ shareValue: bigOr(r.shareValue), threshold: bigOr(r.threshold), progress: numOr(r.progress), stateBlock: bigOr(r.stateBlock), stateTime: secsOr(r.stateTime),
179
+ steps: num(r.steps), burntTotal: big(r.burntTotal), cutTotal: big(r.cutTotal), paidTotal: big(r.paidTotal), deepenedTotal: big(r.deepenedTotal), lastStep: secsOr(r.lastStep),
180
+ }));
181
+ }
182
+ export async function protocolRevenueDays(url, q = {}) {
183
+ const rows = await viewRows(url, "allProtocolRevenueDays", "ProtocolRevenueDay", "day side source basis token amount amountQuote events", q);
184
+ return rows.map((r) => ({
185
+ day: secs(r.day), side: r.side, source: String(r.source), basis: r.basis, token: addr(r.token),
186
+ amount: big(r.amount), amountQuote: big(r.amountQuote), events: num(r.events),
187
+ }));
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logswap/sdk",
3
- "version": "0.5.75",
3
+ "version": "0.5.76",
4
4
  "description": "TypeScript SDK for Logswap, the liquidity primitive introducing the logarithmic invariant — typed wrappers over the CPoolManager / FPoolManager / Router / Lens surface.",
5
5
  "license": "GPL-2.0-or-later",
6
6
  "type": "module",
package/src/fees.ts CHANGED
@@ -15,6 +15,22 @@ import type { LogswapClient } from "./client.js";
15
15
  import { cPoolManagerAbi } from "./generated.js";
16
16
  import type { DiscoveredMarket } from "./pools.js";
17
17
  import { getEventLogs } from "./history.js";
18
+ import { swapFlows } from "./views.js";
19
+
20
+ /** A C market's swap fees over [fromBlock, toBlock] from the indexer's swap_flow view; null when the indexer cannot answer. */
21
+ async function windowFromView(c: LogswapClient, indexer: string, poolId: string, fromBlock: bigint, toBlock: bigint): Promise<{ fees: bigint; swaps: number } | null> {
22
+ try {
23
+ const rows = await swapFlows(indexer, {
24
+ condition: { side: "C", poolId: poolId.toLowerCase(), manager: c.addresses.cPoolManager.toLowerCase() },
25
+ filter: { blockNumber: { greaterThanOrEqualTo: fromBlock.toString(), lessThanOrEqualTo: toBlock.toString() } },
26
+ });
27
+ let fees = 0n;
28
+ for (const r of rows) fees += r.feeQuote;
29
+ return { fees, swaps: rows.length };
30
+ } catch {
31
+ return null; // the view is a cache: the Swap rows answer the same
32
+ }
33
+ }
18
34
 
19
35
  const WAD = 10n ** 18n;
20
36
  const YEAR_SECONDS = 31_536_000;
@@ -86,7 +102,7 @@ export async function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint, hi
86
102
  export async function swapFees(
87
103
  c: LogswapClient,
88
104
  m: DiscoveredMarket,
89
- opts: { since?: bigint; fromBlock?: bigint; toBlock?: bigint; /** The rindexer GraphQL endpoint: the Swap rows from there, the node otherwise. */ indexer?: string } = {},
105
+ opts: { since?: bigint; fromBlock?: bigint; toBlock?: bigint; /** The rindexer GraphQL endpoint: the window from its swap_flow view (the Swap rows as fallback), the node otherwise. */ indexer?: string } = {},
90
106
  ): Promise<SwapFeeWindow> {
91
107
  const toBlock = opts.toBlock ?? (await c.public.getBlockNumber());
92
108
  const [opened, last] = await Promise.all([
@@ -111,16 +127,21 @@ export async function swapFees(
111
127
  const ev = cPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Swap");
112
128
  if (!ev) throw new Error("logswap: Swap event missing from the generated ABI");
113
129
 
114
- const logs = fromBlock > toBlock
115
- ? []
116
- : (await getEventLogs<{ fee: bigint }>(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock })).logs;
117
-
118
- // the event carries the fee the swap charged (decisions 050), exactly — no re-derivation from φ
130
+ // the event carries the fee the swap charged (decisions 050), exactly — no re-derivation from φ;
131
+ // the indexer's swap_flow view carries the same figure per swap (`feeQuote`, C)
119
132
  let fees = 0n;
120
- for (const l of logs) fees += l.args.fee;
133
+ let swaps = 0;
134
+ const viewed = fromBlock > toBlock || !opts.indexer ? null : await windowFromView(c, opts.indexer, m.poolId, fromBlock, toBlock);
135
+ if (viewed) {
136
+ ({ fees, swaps } = viewed);
137
+ } else if (fromBlock <= toBlock) {
138
+ const { logs } = await getEventLogs<{ fee: bigint }>(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock });
139
+ for (const l of logs) fees += l.args.fee;
140
+ swaps = logs.length;
141
+ }
121
142
 
122
143
  const to = last.timestamp;
123
- return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps: logs.length };
144
+ return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps };
124
145
  }
125
146
 
126
147
  /**
package/src/fpool.ts CHANGED
@@ -50,6 +50,7 @@ export const fPolicyBaseAbi = polPolicyAbi;
50
50
  import type { LogswapClient } from "./client.js";
51
51
  import { fallBackUnlessRejected, signerFor } from "./write.js";
52
52
  import { getEventLogs } from "./history.js";
53
+ import { poolHolders } from "./views.js";
53
54
  import { presetMintFee } from "./positions.js";
54
55
 
55
56
  const WAD = 10n ** 18n;
@@ -2016,7 +2017,7 @@ export async function fPoolsControlledBy(
2016
2017
  /**
2017
2018
  * Every holder of a pool's shares, from the ERC-6909 Transfer stream — the id is an indexed
2018
2019
  * topic, so one filtered log query aggregates the whole ledger. The 0xdead entry is the
2019
- * MIN_SHARES lock from seeding; callers usually label rather than hide it. Pass `indexer`: a node
2020
+ * MIN_SHARES lock from seeding; callers usually label rather than hide it. Pass `indexer`: the indexer's `pool_holder` view answers directly (the Transfer replay is the fallback) — a node
2020
2021
  * prunes its logs (anvil keeps about a day), and a ledger rebuilt from a pruned stream loses every
2021
2022
  * holder minted before the cut.
2022
2023
  */
@@ -2028,7 +2029,19 @@ export async function fPoolShareHolders(
2028
2029
  type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "Transfer" }>;
2029
2030
  const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Transfer");
2030
2031
  if (!ev) throw new Error("logswap: 6909 Transfer event missing from the generated ABI");
2031
- const id = fPoolShareId(poolIdHex, opts.gen ?? (await fPoolGen(c, poolIdHex)));
2032
+ const gen = opts.gen ?? (await fPoolGen(c, poolIdHex));
2033
+ if (opts.indexer) {
2034
+ try {
2035
+ const rows = await poolHolders(opts.indexer, { condition: { side: "F", poolId: poolIdHex.toLowerCase(), gen: Number(gen), manager: c.addresses.fPoolManager!.toLowerCase() } });
2036
+ return rows
2037
+ .filter((r) => r.balance > 0n)
2038
+ .map((r) => ({ holder: r.holder, shares: r.balance }))
2039
+ .sort((a, b) => (b.shares > a.shares ? 1 : -1));
2040
+ } catch {
2041
+ /* the view is a cache: the Transfer replay below answers the same */
2042
+ }
2043
+ }
2044
+ const id = fPoolShareId(poolIdHex, gen);
2032
2045
  const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.fPoolManager!, event: ev, args: { id }, fromBlock: opts.fromBlock });
2033
2046
  const bal = new Map<string, bigint>();
2034
2047
  for (const l of logs) {
package/src/index.ts CHANGED
@@ -21,5 +21,6 @@ export * from "./tokencache.js";
21
21
  export * from "./fpool.js";
22
22
  export * from "./fees.js";
23
23
  export * from "./history.js";
24
+ export * from "./views.js";
24
25
  export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
25
26
  export * from "./admin.js";
package/src/positions.ts CHANGED
@@ -17,6 +17,7 @@ const C_POSITIONS_SLOT = 4n;
17
17
  const U128 = (1n << 128n) - 1n;
18
18
  import { isPosition, NO_CAP, positionId, unpackFloorCap } from "./ids.js";
19
19
  import { getEventLogs } from "./history.js";
20
+ import { poolHolders } from "./views.js";
20
21
 
21
22
  /** A class's aggregate state — the whole class, not one holder's slice. */
22
23
  export interface PositionClass {
@@ -263,6 +264,22 @@ export async function positionsOf(c: LogswapClient, key: PoolKey, owner: Address
263
264
 
264
265
  const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes32 indexed poolId, int256 floor, int256 cap, bool capped)");
265
266
 
267
+ /** The class ids `owner` holds in `pool`, from the indexer's `pool_holder` view; null when the indexer cannot answer. */
268
+ async function heldIdsFromView(c: LogswapClient, indexer: string, pool: Hex, owner: Address): Promise<bigint[] | null> {
269
+ try {
270
+ const rows = await poolHolders(indexer, { condition: { side: "C", poolId: pool.toLowerCase(), holder: owner.toLowerCase(), manager: c.addresses.cPoolManager.toLowerCase() } });
271
+ return rows.filter((r) => r.balance > 0n && !r.isBackstop).map((r) => r.id);
272
+ } catch {
273
+ return null; // the view is a cache: the IdRegistered replay below answers the same
274
+ }
275
+ }
276
+
277
+ /** Every class id ever registered in `pool` (`IdRegistered`), from the indexer's table or the node. */
278
+ async function registeredIds(c: LogswapClient, pool: Hex, fromBlock: bigint, indexer?: string): Promise<bigint[]> {
279
+ const { logs } = await getEventLogs<{ id?: bigint }>(c, { indexer, address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock });
280
+ return logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
281
+ }
282
+
266
283
  /**
267
284
  * EVERY class this owner holds in a pool, capped ones included — the complete book.
268
285
  *
@@ -273,10 +290,9 @@ const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes
273
290
  * holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
274
291
  * a read per registered class; fine for a pool's lifetime of classes.
275
292
  */
276
- export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n, opts: { /** The rindexer GraphQL endpoint: the class registry from there, the node otherwise. */ indexer?: string } = {}): Promise<HeldPositionRow[]> {
293
+ export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n, opts: { /** The rindexer GraphQL endpoint: the owner's held ids from its `pool_holder` view (the class registry as fallback), the node otherwise. */ indexer?: string } = {}): Promise<HeldPositionRow[]> {
277
294
  const pool = poolId(key);
278
- const { logs } = await getEventLogs<{ id?: bigint }>(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock });
279
- const ids = logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
295
+ const ids = (opts.indexer ? await heldIdsFromView(c, opts.indexer, pool, owner) : null) ?? (await registeredIds(c, pool, fromBlock, opts.indexer));
280
296
  if (ids.length === 0) return [];
281
297
  // one lens call: balance, class L, fees, floor and cap for every id the owner holds
282
298
  const rows = (await c.public.readContract({