@logswap/sdk 0.5.74 → 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/README.md CHANGED
@@ -69,6 +69,53 @@ Since logswap-contract #60 (decisions 064–065) creation is curated at launch,
69
69
  collector's helpers: `policySetCreatorListed`, `policySetMintFeeListed`, `gateSetPairListed`,
70
70
  `gateSetSpacingListed`, `gateSetForwarderTrusted`, `setCGate`.
71
71
 
72
+ ## History from the indexer (indexer phase 1)
73
+
74
+ Every event read goes through `getEventLogs` (one event) or `getEventsLogs` (several, tagged by
75
+ `eventName`): the rindexer GraphQL endpoint when one is given, the node otherwise or when it fails.
76
+ Since logswap-be #43 the indexer also holds the four policies (one `Policy` table per event, tracked
77
+ from the registry's `Approved`), the registry, the token factory and the managers' admin events, so
78
+ nothing has to scan the node from block 0 — which the devnet node, pruning logs after about a day,
79
+ answers only partly.
80
+
81
+ - `swapFees`, `holdingsOf`, `discoverMarkets` and `fPoolKeyOfPool` take `indexer` (the salt is not in
82
+ `FInitialize`, so a pad's key is still rebuilt from its split with nonce 0).
83
+ - `getEventLogs` takes several emitters (`address: [...]`); a policy outside the artifact names its
84
+ shared tables with `contract: indexedContractOf(kind, event)` (or `kind` on `getEventsLogs`).
85
+ - `tokensCreated` lists the factory's tokens (`addresses.tokenFactory`): the token list's provenance.
86
+
87
+ ## Indexer views (indexing phase 2)
88
+
89
+ The indexer also serves derived views (logswap-be `devnet/VIEWS.md`), joining its event tables with
90
+ the snapshotter's state. `views.ts` reads each as typed rows: amounts as `bigint`, prices, APYs and
91
+ progress as `number`, times as unix seconds. Every reader takes the GraphQL endpoint and a
92
+ `{ condition, filter, orderBy, first, signal }` query, and pages by offset.
93
+
94
+ | Reader | View | What a row is |
95
+ | --- | --- | --- |
96
+ | `poolDirectory` | `allPoolDirectories` | a pool of either manager, its kind and birth |
97
+ | `eventUsers` | `allEventUsers` | a manager event and who did it (the router's caller when routed) |
98
+ | `poolHolders` | `allPoolHolders` | a current ERC-6909 balance, with C's backstop and F's dead shares flagged |
99
+ | `walletPositions` | `allWalletPositions` | a wallet's holding; F valued at the latest share value |
100
+ | `walletPoolFlows` | `allWalletPoolFlows` | a wallet's in/out, fees and dividends per pool |
101
+ | `swapFlows` | `allSwapFlows` | a swap's volume and fee in quote |
102
+ | `priceTicks` | `allPriceTicks` | a price point per (pool, base) |
103
+ | `candles(url, "minute" \| "hour" \| "day")` | `allCandle…s` | OHLC with volume and fees |
104
+ | `poolTvlHours` | `allPoolTvlHours` | the hour's last snapshot: TVL, liquidity |
105
+ | `poolSummaries` | `allPoolSummaries` | 24 h / 7 d volume, fees, APY; latest TVL |
106
+ | `generationSteps` | `allGenerationSteps` | a `regenerate` step and its pot split |
107
+ | `padStatuses` | `allPadStatuses` | each pad and POL now, with the next threshold and progress |
108
+ | `protocolRevenueDays` | `allProtocolRevenueDays` | protocol revenue per day and source |
109
+
110
+ Given an `indexer`, three functions now take their figure from a view and keep their previous path
111
+ as the fallback, with the same answer on the same data:
112
+ - `holdingsOf`: the owner's held class ids from `pool_holder`, then the lens as before;
113
+ - `fPoolShareHolders`: the holders from `pool_holder`;
114
+ - `swapFees`: the window from `swap_flow`.
115
+
116
+ What stays a live read: a C position's value and every unclaimed fee or dividend (the lens: `legsOf`,
117
+ `heldByIds`, `fDividendOf`). They accrue without events, so no view holds them.
118
+
72
119
  ## Capped F pools and the lien (F version 25)
73
120
 
74
121
  - **The cap** (decisions 061) is a key field: `FPoolKey.cap` / `FPoolCreateArgs.cap`, d =
package/dist/client.d.ts CHANGED
@@ -35,6 +35,8 @@ export interface LogswapAddresses {
35
35
  * absent; a deployment with none asks nobody.
36
36
  */
37
37
  policyRegistry?: Address;
38
+ /** The devnet's token factory (`TokenFactory`, a mock): where launchable tokens come from. Optional. */
39
+ tokenFactory?: Address;
38
40
  /** Optional — the router falls back to plain approvals when absent. */
39
41
  permit2?: Address;
40
42
  }
package/dist/fees.d.ts CHANGED
@@ -55,7 +55,8 @@ export declare function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint,
55
55
  export declare function swapFees(c: LogswapClient, m: DiscoveredMarket, opts?: {
56
56
  since?: bigint;
57
57
  fromBlock?: bigint;
58
- toBlock?: bigint;
58
+ toBlock?: bigint; /** The rindexer GraphQL endpoint: the window from its swap_flow view (the Swap rows as fallback), the node otherwise. */
59
+ indexer?: string;
59
60
  }): Promise<SwapFeeWindow>;
60
61
  /**
61
62
  * The window's swap fees over the market's CURRENT active L, annualised linearly over the window.
package/dist/fees.js CHANGED
@@ -11,6 +11,24 @@
11
11
  * Both equal φ·L·|Δx| summed over the swap's segments — the same φ·|dx| per unit L that `F` books.
12
12
  */
13
13
  import { cPoolManagerAbi } from "./generated.js";
14
+ import { getEventLogs } from "./history.js";
15
+ import { swapFlows } from "./views.js";
16
+ /** A C market's swap fees over [fromBlock, toBlock] from the indexer's swap_flow view; null when the indexer cannot answer. */
17
+ async function windowFromView(c, indexer, poolId, fromBlock, toBlock) {
18
+ try {
19
+ const rows = await swapFlows(indexer, {
20
+ condition: { side: "C", poolId: poolId.toLowerCase(), manager: c.addresses.cPoolManager.toLowerCase() },
21
+ filter: { blockNumber: { greaterThanOrEqualTo: fromBlock.toString(), lessThanOrEqualTo: toBlock.toString() } },
22
+ });
23
+ let fees = 0n;
24
+ for (const r of rows)
25
+ fees += r.feeQuote;
26
+ return { fees, swaps: rows.length };
27
+ }
28
+ catch {
29
+ return null; // the view is a cache: the Swap rows answer the same
30
+ }
31
+ }
14
32
  const WAD = 10n ** 18n;
15
33
  const YEAR_SECONDS = 31_536_000;
16
34
  /**
@@ -87,15 +105,22 @@ export async function swapFees(c, m, opts = {}) {
87
105
  const ev = cPoolManagerAbi.find((x) => x.type === "event" && x.name === "Swap");
88
106
  if (!ev)
89
107
  throw new Error("logswap: Swap event missing from the generated ABI");
90
- const logs = fromBlock > toBlock
91
- ? []
92
- : await c.public.getLogs({ address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock });
93
- // the event carries the fee the swap charged (decisions 050), exactly — no re-derivation from φ
108
+ // the event carries the fee the swap charged (decisions 050), exactly — no re-derivation from φ;
109
+ // the indexer's swap_flow view carries the same figure per swap (`feeQuote`, C)
94
110
  let fees = 0n;
95
- for (const l of logs)
96
- fees += l.args.fee;
111
+ let swaps = 0;
112
+ const viewed = fromBlock > toBlock || !opts.indexer ? null : await windowFromView(c, opts.indexer, m.poolId, fromBlock, toBlock);
113
+ if (viewed) {
114
+ ({ fees, swaps } = viewed);
115
+ }
116
+ else if (fromBlock <= toBlock) {
117
+ const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock });
118
+ for (const l of logs)
119
+ fees += l.args.fee;
120
+ swaps = logs.length;
121
+ }
97
122
  const to = last.timestamp;
98
- return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps: logs.length };
123
+ return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps };
99
124
  }
100
125
  /**
101
126
  * The window's swap fees over the market's CURRENT active L, annualised linearly over the window.
package/dist/fpool.d.ts CHANGED
@@ -1412,7 +1412,8 @@ export declare function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "ac
1412
1412
  */
1413
1413
  export declare function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts?: {
1414
1414
  salt?: Hex;
1415
- fromBlock?: bigint;
1415
+ fromBlock?: bigint; /** The rindexer GraphQL endpoint: `FInitialize` from there, the node otherwise. The salt is in neither (the event does not carry it). */
1416
+ indexer?: string;
1416
1417
  }): Promise<FPoolKey>;
1417
1418
  /**
1418
1419
  * The reserve a leg holds at a mark: `R = Lj·e^{−x}`, WAD math. The seed names RESERVES —
@@ -1913,7 +1914,7 @@ export declare function fPoolsControlledBy(c: LogswapClient, who: Address, opts?
1913
1914
  /**
1914
1915
  * Every holder of a pool's shares, from the ERC-6909 Transfer stream — the id is an indexed
1915
1916
  * topic, so one filtered log query aggregates the whole ledger. The 0xdead entry is the
1916
- * MIN_SHARES lock from seeding; callers usually label rather than hide it. Pass `indexer`: a node
1917
+ * 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
1917
1918
  * prunes its logs (anvil keeps about a day), and a ledger rebuilt from a pruned stream loses every
1918
1919
  * holder minted before the cut.
1919
1920
  */
package/dist/fpool.js CHANGED
@@ -47,6 +47,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
47
47
  export const fPolicyBaseAbi = polPolicyAbi;
48
48
  import { fallBackUnlessRejected, signerFor } from "./write.js";
49
49
  import { getEventLogs } from "./history.js";
50
+ import { poolHolders } from "./views.js";
50
51
  import { presetMintFee } from "./positions.js";
51
52
  const WAD = 10n ** 18n;
52
53
  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
@@ -779,13 +780,7 @@ export async function fPoolIdOf(c, a) {
779
780
  */
780
781
  export async function fPoolKeyOfPool(c, poolId, opts = {}) {
781
782
  const ev = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "FInitialize");
782
- const logs = await c.public.getLogs({
783
- address: c.addresses.fPoolManager,
784
- event: ev,
785
- args: { poolId },
786
- fromBlock: opts.fromBlock ?? 0n,
787
- toBlock: "latest",
788
- });
783
+ const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.fPoolManager, event: ev, args: { poolId }, fromBlock: opts.fromBlock ?? 0n });
789
784
  const l = logs[0];
790
785
  if (!l)
791
786
  throw new Error(`logswap: no FInitialize for F pool ${poolId}`);
@@ -1428,7 +1423,7 @@ export async function fPoolsControlledBy(c, who, opts = {}) {
1428
1423
  /**
1429
1424
  * Every holder of a pool's shares, from the ERC-6909 Transfer stream — the id is an indexed
1430
1425
  * topic, so one filtered log query aggregates the whole ledger. The 0xdead entry is the
1431
- * MIN_SHARES lock from seeding; callers usually label rather than hide it. Pass `indexer`: a node
1426
+ * 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
1432
1427
  * prunes its logs (anvil keeps about a day), and a ledger rebuilt from a pruned stream loses every
1433
1428
  * holder minted before the cut.
1434
1429
  */
@@ -1436,7 +1431,20 @@ export async function fPoolShareHolders(c, poolIdHex, opts = {}) {
1436
1431
  const ev = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "Transfer");
1437
1432
  if (!ev)
1438
1433
  throw new Error("logswap: 6909 Transfer event missing from the generated ABI");
1439
- const id = fPoolShareId(poolIdHex, opts.gen ?? (await fPoolGen(c, poolIdHex)));
1434
+ const gen = opts.gen ?? (await fPoolGen(c, poolIdHex));
1435
+ if (opts.indexer) {
1436
+ try {
1437
+ const rows = await poolHolders(opts.indexer, { condition: { side: "F", poolId: poolIdHex.toLowerCase(), gen: Number(gen), manager: c.addresses.fPoolManager.toLowerCase() } });
1438
+ return rows
1439
+ .filter((r) => r.balance > 0n)
1440
+ .map((r) => ({ holder: r.holder, shares: r.balance }))
1441
+ .sort((a, b) => (b.shares > a.shares ? 1 : -1));
1442
+ }
1443
+ catch {
1444
+ /* the view is a cache: the Transfer replay below answers the same */
1445
+ }
1446
+ }
1447
+ const id = fPoolShareId(poolIdHex, gen);
1440
1448
  const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.fPoolManager, event: ev, args: { id }, fromBlock: opts.fromBlock });
1441
1449
  const bal = new Map();
1442
1450
  for (const l of logs) {
package/dist/history.d.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { type AbiEvent, type Address, type Hex } from "viem";
17
17
  import type { LogswapClient } from "./client.js";
18
+ import type { FPoolKind } from "./fpool.js";
18
19
  /** One event, as `getLogs` returns it plus its block's timestamp (unix seconds). */
19
20
  export interface HistoryLog<A = Record<string, unknown>> {
20
21
  address: Address;
@@ -28,7 +29,14 @@ export interface HistoryLog<A = Record<string, unknown>> {
28
29
  export interface EventLogsQuery {
29
30
  /** The rindexer GraphQL endpoint; absent means the chain. */
30
31
  indexer?: string;
31
- address: Address;
32
+ /** The emitter, or several: the four policies share one table per event (indexer phase 1). */
33
+ address: Address | readonly Address[];
34
+ /**
35
+ * The emitter's name in the indexer's config, for an event two indexed contracts share — only
36
+ * needed when the address is not in the client's artifact (a policy the registry approved later):
37
+ * see {@link indexedContractOf}.
38
+ */
39
+ contract?: string;
32
40
  event: AbiEvent;
33
41
  /** Indexed arguments to match, as `getLogs` takes them; an array matches any of its values. */
34
42
  args?: Record<string, unknown>;
@@ -53,9 +61,81 @@ export declare function getEventLogs<A = Record<string, unknown>>(c: LogswapClie
53
61
  logs: HistoryLog<A>[];
54
62
  source: "indexer" | "chain";
55
63
  }>;
56
- export declare function tableFor(c: LogswapClient, address: Address, event: string): {
64
+ /**
65
+ * A policy's name in the indexer for one of its events: `Policy`, or the kind's own contract for the
66
+ * two-signature `Listed`. Pass it as {@link EventLogsQuery.contract} for a policy outside the artifact.
67
+ */
68
+ export declare function indexedContractOf(kind: FPoolKind, event: string): string;
69
+ export declare function tableFor(c: LogswapClient, address: Address, event: string, contract?: string): {
57
70
  field: string;
58
71
  type: string;
59
72
  };
60
73
  /** How far back the chain path looks for a `latest` tape: a tape is recent by definition. */
61
74
  export declare const LATEST_SCAN_BLOCKS = 5000n;
75
+ /** A row of {@link getEventsLogs}: one of the asked events, named. */
76
+ export interface NamedHistoryLog extends HistoryLog {
77
+ eventName: string;
78
+ }
79
+ /**
80
+ * Several events of one contract (or of several emitters sharing them), merged oldest first and
81
+ * tagged with `eventName` — the shape of viem's multi-event `getLogs`, which is what the chain path
82
+ * runs. One indexer table per event; if any of them cannot answer, the whole read goes to the node,
83
+ * so a history is never half the indexer's and half the node's.
84
+ */
85
+ export declare function getEventsLogs(c: LogswapClient, q: Omit<EventLogsQuery, "event" | "args" | "latest" | "contract" | "transactionHashes"> & {
86
+ events: readonly AbiEvent[];
87
+ /** The emitters' policy kind, to name a shared event's table for a policy outside the artifact. */
88
+ kind?: FPoolKind;
89
+ /** The emitter's name in the indexer (e.g. `PolicyRegistry`), for a contract outside the artifact; `kind` wins for a policy. */
90
+ contract?: string;
91
+ }): Promise<{
92
+ logs: NamedHistoryLog[];
93
+ source: "indexer" | "chain";
94
+ }>;
95
+ /** The devnet's token factory event (`src/mocks/TokenFactory.sol`): provenance for every launchable token. */
96
+ export declare const TOKEN_CREATED: {
97
+ readonly name: "TokenCreated";
98
+ readonly type: "event";
99
+ readonly inputs: readonly [{
100
+ readonly type: "address";
101
+ readonly name: "creator";
102
+ readonly indexed: true;
103
+ }, {
104
+ readonly type: "address";
105
+ readonly name: "token";
106
+ readonly indexed: true;
107
+ }, {
108
+ readonly type: "string";
109
+ readonly name: "name";
110
+ }, {
111
+ readonly type: "string";
112
+ readonly name: "symbol";
113
+ }, {
114
+ readonly type: "uint256";
115
+ readonly name: "supply";
116
+ }];
117
+ };
118
+ export interface CreatedToken {
119
+ creator: Address;
120
+ token: Address;
121
+ name: string;
122
+ symbol: string;
123
+ supply: bigint;
124
+ blockNumber: bigint;
125
+ /** Unix seconds; set when the indexer answered. */
126
+ blockTimestamp?: number;
127
+ transactionHash: Hex;
128
+ }
129
+ /**
130
+ * Every token the deployment's factory created, oldest first — the token list's provenance (a token
131
+ * named like a factory token but deployed elsewhere is not in it). Optionally one creator's.
132
+ */
133
+ export declare function tokensCreated(c: LogswapClient, q: {
134
+ indexer?: string;
135
+ creator?: Address;
136
+ fromBlock?: bigint;
137
+ signal?: AbortSignal;
138
+ }): Promise<{
139
+ tokens: CreatedToken[];
140
+ source: "indexer" | "chain";
141
+ }>;
package/dist/history.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * the decoding (the inputs' types — numbers as bigint, addresses checksummed, arrays from JSON).
14
14
  * With no indexer URL, or when the query fails, it asks the node and says so in `source`.
15
15
  */
16
- import { getAddress } from "viem";
16
+ import { getAddress, parseAbiItem } from "viem";
17
17
  import { bytea, gql } from "./indexer.js";
18
18
  import { blockAtOrAfter } from "./fees.js";
19
19
  /**
@@ -32,29 +32,57 @@ export async function getEventLogs(c, q) {
32
32
  return { logs: await fromChain(c, q), source: "chain" };
33
33
  }
34
34
  // ─── the indexer path ────────────────────────────────────────────────────────
35
- /** What the rindexer deployment calls each manager — the prefix of a table two contracts share. */
36
- function contractName(c, address) {
35
+ /**
36
+ * What the rindexer deployment calls each contract — the prefix of a table two contracts share
37
+ * (logswap-be `devnet/indexer/rindexer.yaml`). The four policies are one factory-tracked contract,
38
+ * `Policy`, except `Listed`, whose two signatures are two contracts (`PublicPolicy`, `PrivatePolicy`).
39
+ */
40
+ function contractName(c, address, event) {
37
41
  const a = address.toLowerCase();
38
- if (a === c.addresses.cPoolManager.toLowerCase())
42
+ const is = (x) => x !== undefined && a === x.toLowerCase();
43
+ const { addresses: d } = c;
44
+ if (is(d.cPoolManager))
39
45
  return "LogswapManager";
40
- if (c.addresses.fPoolManager && a === c.addresses.fPoolManager.toLowerCase())
46
+ if (is(d.fPoolManager))
41
47
  return "FPoolManager";
42
- if (a === c.addresses.router.toLowerCase())
48
+ if (is(d.router))
43
49
  return "LogswapRouter";
50
+ if (is(d.policyRegistry))
51
+ return "PolicyRegistry";
52
+ if (is(d.tokenFactory))
53
+ return "TokenFactory";
54
+ if (is(d.publicPolicy))
55
+ return indexedContractOf("public", event);
56
+ if (is(d.privatePolicy))
57
+ return indexedContractOf("private", event);
58
+ if (is(d.polPolicy))
59
+ return indexedContractOf("pol", event);
60
+ if (is(d.padPolicy))
61
+ return indexedContractOf("pad", event);
44
62
  return null;
45
63
  }
64
+ /**
65
+ * A policy's name in the indexer for one of its events: `Policy`, or the kind's own contract for the
66
+ * two-signature `Listed`. Pass it as {@link EventLogsQuery.contract} for a policy outside the artifact.
67
+ */
68
+ export function indexedContractOf(kind, event) {
69
+ if (event === "Listed")
70
+ return kind === "private" ? "PrivatePolicy" : "PublicPolicy";
71
+ return "Policy";
72
+ }
46
73
  /** Events whose name two indexed contracts share: postgraphile prefixes their tables. */
47
- const SHARED = new Set(["Transfer"]);
74
+ const SHARED = new Set(["Transfer", "Approval", "OperatorSet", "CollectorProposed", "CollectorAccepted", "Approved", "Listed"]);
48
75
  const plural = (n) => (/(s|x|ch|sh)$/.test(n) ? `${n}es` : `${n}s`);
49
76
  /** A column's GraphQL name: postgraphile camelCases the snake_case rindexer writes (`F` → `f`). */
50
77
  const column = (name) => name[0].toLowerCase() + name.slice(1);
51
- export function tableFor(c, address, event) {
52
- const prefix = SHARED.has(event) ? contractName(c, address) : "";
78
+ export function tableFor(c, address, event, contract) {
79
+ const prefix = SHARED.has(event) ? (contract ?? contractName(c, address, event)) : "";
53
80
  if (prefix === null)
54
81
  throw new Error(`history: no indexed contract at ${address}`);
55
82
  const type = `${prefix}${event}`;
56
83
  return { field: `all${plural(type)}`, type };
57
84
  }
85
+ const emitters = (q) => (Array.isArray(q.address) ? [...q.address] : [q.address]);
58
86
  function decode(type, v) {
59
87
  if (v === null || v === undefined)
60
88
  return v;
@@ -71,7 +99,10 @@ const MAX_PAGES = 200;
71
99
  /** Pages in flight at once, after the first. */
72
100
  const PARALLEL_PAGES = 8;
73
101
  async function fromIndexer(c, q) {
74
- const { field, type } = tableFor(c, q.address, q.event.name);
102
+ const all = emitters(q);
103
+ if (all.length === 0)
104
+ return [];
105
+ const { field, type } = tableFor(c, all[0], q.event.name, q.contract);
75
106
  const inputs = q.event.inputs;
76
107
  const condition = {};
77
108
  const filter = {};
@@ -134,11 +165,11 @@ async function fromIndexer(c, q) {
134
165
  }
135
166
  }
136
167
  // a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
137
- const want = q.address.toLowerCase();
168
+ const want = new Map(all.map((a) => [a.toLowerCase(), a]));
138
169
  return rows
139
- .filter((r) => String(r.contractAddress).toLowerCase() === want)
170
+ .filter((r) => want.has(String(r.contractAddress).toLowerCase()))
140
171
  .map((r) => ({
141
- address: q.address,
172
+ address: want.get(String(r.contractAddress).toLowerCase()),
142
173
  args: Object.fromEntries(inputs.map((i) => [i.name, decode(i.type, r[column(i.name)])])),
143
174
  blockNumber: BigInt(r.blockNumber),
144
175
  blockTimestamp: Math.floor(Date.parse(r.blockTimestamp) / 1000),
@@ -152,12 +183,15 @@ async function fromIndexer(c, q) {
152
183
  export const LATEST_SCAN_BLOCKS = 5000n;
153
184
  async function fromChain(c, q) {
154
185
  let fromBlock = q.fromBlock ?? 0n;
155
- const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
186
+ const toBlock = q.toBlock ?? (await c.public.getBlockNumber({ cacheTime: 0 })); // uncached: viem caches the head, and a scan bounded by a stale one misses the block just mined
156
187
  if (q.since !== undefined)
157
188
  fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
158
189
  if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS)
159
190
  fromBlock = toBlock - LATEST_SCAN_BLOCKS;
160
- let logs = (await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock }));
191
+ const all = emitters(q);
192
+ if (all.length === 0)
193
+ return [];
194
+ let logs = (await c.public.getLogs({ address: Array.isArray(q.address) ? all : q.address, event: q.event, args: q.args, fromBlock, toBlock }));
161
195
  if (q.transactionHashes) {
162
196
  const want = new Set(q.transactionHashes.map((h) => h.toLowerCase()));
163
197
  logs = logs.filter((l) => want.has(l.transactionHash.toLowerCase()));
@@ -165,10 +199,68 @@ async function fromChain(c, q) {
165
199
  if (q.latest !== undefined)
166
200
  logs = logs.slice(-q.latest);
167
201
  return logs.map((l) => ({
168
- address: q.address,
202
+ address: l.address ? getAddress(l.address) : all[0],
169
203
  args: l.args,
170
204
  blockNumber: l.blockNumber,
171
205
  transactionHash: l.transactionHash,
172
206
  logIndex: l.logIndex,
173
207
  }));
174
208
  }
209
+ /**
210
+ * Several events of one contract (or of several emitters sharing them), merged oldest first and
211
+ * tagged with `eventName` — the shape of viem's multi-event `getLogs`, which is what the chain path
212
+ * runs. One indexer table per event; if any of them cannot answer, the whole read goes to the node,
213
+ * so a history is never half the indexer's and half the node's.
214
+ */
215
+ export async function getEventsLogs(c, q) {
216
+ const order = (a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1);
217
+ if (q.indexer && q.events.length > 0) {
218
+ try {
219
+ const got = await Promise.all(q.events.map(async (event) => {
220
+ const contract = q.kind ? indexedContractOf(q.kind, event.name) : q.contract;
221
+ return (await fromIndexer(c, { ...q, event, contract })).map((l) => ({ ...l, eventName: event.name }));
222
+ }));
223
+ return { logs: got.flat().sort(order), source: "indexer" };
224
+ }
225
+ catch {
226
+ /* the indexer is a cache: the node holds the same events */
227
+ }
228
+ }
229
+ const all = emitters(q);
230
+ if (all.length === 0 || q.events.length === 0)
231
+ return { logs: [], source: "chain" };
232
+ let fromBlock = q.fromBlock ?? 0n;
233
+ const toBlock = q.toBlock ?? (await c.public.getBlockNumber({ cacheTime: 0 })); // uncached: viem caches the head, and a scan bounded by a stale one misses the block just mined
234
+ if (q.since !== undefined)
235
+ fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
236
+ const logs = (await c.public.getLogs({ address: Array.isArray(q.address) ? all : q.address, events: q.events, fromBlock, toBlock }));
237
+ return {
238
+ logs: logs
239
+ .map((l) => ({ eventName: l.eventName, address: l.address ? getAddress(l.address) : all[0], args: l.args, blockNumber: l.blockNumber, transactionHash: l.transactionHash, logIndex: l.logIndex }))
240
+ .sort(order),
241
+ source: "chain",
242
+ };
243
+ }
244
+ /** The devnet's token factory event (`src/mocks/TokenFactory.sol`): provenance for every launchable token. */
245
+ export const TOKEN_CREATED = parseAbiItem("event TokenCreated(address indexed creator, address indexed token, string name, string symbol, uint256 supply)");
246
+ /**
247
+ * Every token the deployment's factory created, oldest first — the token list's provenance (a token
248
+ * named like a factory token but deployed elsewhere is not in it). Optionally one creator's.
249
+ */
250
+ export async function tokensCreated(c, q) {
251
+ const factory = c.addresses.tokenFactory;
252
+ if (!factory)
253
+ throw new Error("logswap: tokensCreated needs addresses.tokenFactory");
254
+ const { logs, source } = await getEventLogs(c, {
255
+ indexer: q.indexer,
256
+ address: factory,
257
+ event: TOKEN_CREATED,
258
+ args: q.creator ? { creator: q.creator } : undefined,
259
+ fromBlock: q.fromBlock,
260
+ signal: q.signal,
261
+ });
262
+ return {
263
+ tokens: logs.map((l) => ({ ...l.args, blockNumber: l.blockNumber, ...(l.blockTimestamp !== undefined ? { blockTimestamp: l.blockTimestamp } : {}), transactionHash: l.transactionHash })),
264
+ source,
265
+ };
266
+ }
package/dist/index.d.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/dist/index.js 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/dist/pools.d.ts CHANGED
@@ -119,7 +119,8 @@ export declare function discoverMarkets(c: LogswapClient, opts?: {
119
119
  fromBlock?: bigint;
120
120
  toBlock?: bigint;
121
121
  base?: Address;
122
- quote?: Address;
122
+ quote?: Address; /** The rindexer GraphQL endpoint: `Initialize` from there, the node otherwise. */
123
+ indexer?: string;
123
124
  }): Promise<DiscoveredMarket[]>;
124
125
  /**
125
126
  * Ladder shape: the live FLOORS, in log-price WAD — **not** tick indices.
package/dist/pools.js CHANGED
@@ -17,6 +17,7 @@ import { signerFor } from "./write.js";
17
17
  import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
18
18
  import { poolId } from "./keys.js";
19
19
  import { presetMintFee } from "./positions.js";
20
+ import { getEventLogs } from "./history.js";
20
21
  /** Read a market's full state by key, through the lens (`getPools`; C version 7). All zero, `initialized` false, for a market nobody opened. */
21
22
  export async function getPool(c, key) {
22
23
  const v = (await c.public.readContract({
@@ -111,12 +112,18 @@ export async function discoverMarkets(c, opts = {}) {
111
112
  const ev = cPoolManagerAbi.find((x) => x.type === "event" && x.name === "Initialize");
112
113
  if (!ev)
113
114
  throw new Error("logswap: Initialize event missing from the generated ABI");
114
- const logs = await c.public.getLogs({
115
+ const args = {};
116
+ if (opts.base)
117
+ args.base = opts.base;
118
+ if (opts.quote)
119
+ args.quote = opts.quote;
120
+ const { logs } = await getEventLogs(c, {
121
+ indexer: opts.indexer,
115
122
  address: c.addresses.cPoolManager,
116
123
  event: ev,
117
- args: { base: opts.base, quote: opts.quote },
124
+ args,
118
125
  fromBlock: opts.fromBlock ?? 0n,
119
- toBlock: opts.toBlock ?? "latest",
126
+ toBlock: opts.toBlock,
120
127
  });
121
128
  return logs.map((l) => {
122
129
  const a = l.args;
@@ -166,7 +166,9 @@ export declare function positionsOf(c: LogswapClient, key: PoolKey, owner: Addre
166
166
  * holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
167
167
  * a read per registered class; fine for a pool's lifetime of classes.
168
168
  */
169
- export declare function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock?: bigint): Promise<HeldPositionRow[]>;
169
+ export declare function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock?: bigint, opts?: {
170
+ indexer?: string;
171
+ }): Promise<HeldPositionRow[]>;
170
172
  /** A holding's legs, exact from the lens: base (base units), the quote leg, claimable fees, and
171
173
  * value = base·p + quote + fees in quote units — L + quote + fees for an uncapped active class. */
172
174
  export declare function legsOf(c: LogswapClient, key: PoolKey, id: bigint, holder: Address): Promise<{
package/dist/positions.js CHANGED
@@ -12,6 +12,8 @@ import { FEE_MAX, poolId } from "./keys.js";
12
12
  const C_POSITIONS_SLOT = 4n;
13
13
  const U128 = (1n << 128n) - 1n;
14
14
  import { isPosition, NO_CAP, positionId, unpackFloorCap } from "./ids.js";
15
+ import { getEventLogs } from "./history.js";
16
+ import { poolHolders } from "./views.js";
15
17
  /**
16
18
  * A class's aggregate state, read through `extsload` (C version 7: the manager's public
17
19
  * `positions` mapping is gone behind it, decisions 065). The slot is `CPoolState`'s:
@@ -181,6 +183,21 @@ export async function positionsOf(c, key, owner) {
181
183
  return rows.map((r) => ({ id: r.id, floor: r.floor, cap: NO_CAP, capped: false, shares: r.shares, L: r.L, fees: r.fees }));
182
184
  }
183
185
  const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes32 indexed poolId, int256 floor, int256 cap, bool capped)");
186
+ /** The class ids `owner` holds in `pool`, from the indexer's `pool_holder` view; null when the indexer cannot answer. */
187
+ async function heldIdsFromView(c, indexer, pool, owner) {
188
+ try {
189
+ const rows = await poolHolders(indexer, { condition: { side: "C", poolId: pool.toLowerCase(), holder: owner.toLowerCase(), manager: c.addresses.cPoolManager.toLowerCase() } });
190
+ return rows.filter((r) => r.balance > 0n && !r.isBackstop).map((r) => r.id);
191
+ }
192
+ catch {
193
+ return null; // the view is a cache: the IdRegistered replay below answers the same
194
+ }
195
+ }
196
+ /** Every class id ever registered in `pool` (`IdRegistered`), from the indexer's table or the node. */
197
+ async function registeredIds(c, pool, fromBlock, indexer) {
198
+ const { logs } = await getEventLogs(c, { indexer, address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock });
199
+ return logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
200
+ }
184
201
  /**
185
202
  * EVERY class this owner holds in a pool, capped ones included — the complete book.
186
203
  *
@@ -191,10 +208,9 @@ const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes
191
208
  * holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
192
209
  * a read per registered class; fine for a pool's lifetime of classes.
193
210
  */
194
- export async function holdingsOf(c, key, owner, fromBlock = 0n) {
211
+ export async function holdingsOf(c, key, owner, fromBlock = 0n, opts = {}) {
195
212
  const pool = poolId(key);
196
- const logs = await c.public.getLogs({ address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock, toBlock: "latest" });
197
- const ids = logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
213
+ const ids = (opts.indexer ? await heldIdsFromView(c, opts.indexer, pool, owner) : null) ?? (await registeredIds(c, pool, fromBlock, opts.indexer));
198
214
  if (ids.length === 0)
199
215
  return [];
200
216
  // one lens call: balance, class L, fees, floor and cap for every id the owner holds