@logswap/sdk 0.5.60 → 0.5.61

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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * history.ts — event history from the indexer, shaped like viem's `getLogs`, with the chain as the
3
+ * fallback.
4
+ *
5
+ * Every history read used to be a node `getLogs` from a pool's birth (or block 0) to the head: a
6
+ * full re-download on every page load, seconds each against a distant node, and silently partial
7
+ * once the node pruned its history. rindexer holds the same events with their block timestamps, so
8
+ * a window is a filter rather than a block search and a scan is one paged query.
9
+ *
10
+ * `getEventLogs` takes the ABI event exactly as `getLogs` does and derives the rest from it: the
11
+ * table (`all<Name>s`, or the manager-prefixed name where two contracts share an event name, as the
12
+ * ERC-6909 `Transfer` does), the columns (the inputs' names, camelCased as postgraphile does), and
13
+ * the decoding (the inputs' types — numbers as bigint, addresses checksummed, arrays from JSON).
14
+ * With no indexer URL, or when the query fails, it asks the node and says so in `source`.
15
+ */
16
+ import { type AbiEvent, type Address, type Hex } from "viem";
17
+ import type { LogswapClient } from "./client.js";
18
+ /** One event, as `getLogs` returns it plus its block's timestamp (unix seconds). */
19
+ export interface HistoryLog<A = Record<string, unknown>> {
20
+ address: Address;
21
+ args: A;
22
+ blockNumber: bigint;
23
+ /** Unix seconds. Always set from the indexer; set on the chain path only when a window asked for it. */
24
+ blockTimestamp?: number;
25
+ transactionHash: Hex;
26
+ logIndex: number;
27
+ }
28
+ export interface EventLogsQuery {
29
+ /** The rindexer GraphQL endpoint; absent means the chain. */
30
+ indexer?: string;
31
+ address: Address;
32
+ event: AbiEvent;
33
+ /** Indexed arguments to match, as `getLogs` takes them; an array matches any of its values. */
34
+ args?: Record<string, unknown>;
35
+ fromBlock?: bigint;
36
+ toBlock?: bigint;
37
+ /** Unix seconds: only events at or after this time. On the chain path it becomes a block search. */
38
+ since?: number;
39
+ signal?: AbortSignal;
40
+ }
41
+ /**
42
+ * The events, oldest first, and where they came from. `"indexer"` rows lag the head by the
43
+ * indexer's own lag — a few blocks at most on the devnet (logswap-health checks it).
44
+ */
45
+ export declare function getEventLogs<A = Record<string, unknown>>(c: LogswapClient, q: EventLogsQuery): Promise<{
46
+ logs: HistoryLog<A>[];
47
+ source: "indexer" | "chain";
48
+ }>;
49
+ export declare function tableFor(c: LogswapClient, address: Address, event: string): {
50
+ field: string;
51
+ type: string;
52
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * history.ts — event history from the indexer, shaped like viem's `getLogs`, with the chain as the
3
+ * fallback.
4
+ *
5
+ * Every history read used to be a node `getLogs` from a pool's birth (or block 0) to the head: a
6
+ * full re-download on every page load, seconds each against a distant node, and silently partial
7
+ * once the node pruned its history. rindexer holds the same events with their block timestamps, so
8
+ * a window is a filter rather than a block search and a scan is one paged query.
9
+ *
10
+ * `getEventLogs` takes the ABI event exactly as `getLogs` does and derives the rest from it: the
11
+ * table (`all<Name>s`, or the manager-prefixed name where two contracts share an event name, as the
12
+ * ERC-6909 `Transfer` does), the columns (the inputs' names, camelCased as postgraphile does), and
13
+ * the decoding (the inputs' types — numbers as bigint, addresses checksummed, arrays from JSON).
14
+ * With no indexer URL, or when the query fails, it asks the node and says so in `source`.
15
+ */
16
+ import { getAddress } from "viem";
17
+ import { bytea, gql } from "./indexer.js";
18
+ import { blockAtOrAfter } from "./fees.js";
19
+ /**
20
+ * The events, oldest first, and where they came from. `"indexer"` rows lag the head by the
21
+ * indexer's own lag — a few blocks at most on the devnet (logswap-health checks it).
22
+ */
23
+ export async function getEventLogs(c, q) {
24
+ if (q.indexer) {
25
+ try {
26
+ return { logs: await fromIndexer(c, q), source: "indexer" };
27
+ }
28
+ catch {
29
+ /* the indexer is a cache: when it cannot answer, the node can */
30
+ }
31
+ }
32
+ return { logs: await fromChain(c, q), source: "chain" };
33
+ }
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) {
37
+ const a = address.toLowerCase();
38
+ if (a === c.addresses.cPoolManager.toLowerCase())
39
+ return "LogswapManager";
40
+ if (c.addresses.fPoolManager && a === c.addresses.fPoolManager.toLowerCase())
41
+ return "FPoolManager";
42
+ if (a === c.addresses.router.toLowerCase())
43
+ return "LogswapRouter";
44
+ return null;
45
+ }
46
+ /** Events whose name two indexed contracts share: postgraphile prefixes their tables. */
47
+ const SHARED = new Set(["Transfer"]);
48
+ const plural = (n) => (/(s|x|ch|sh)$/.test(n) ? `${n}es` : `${n}s`);
49
+ /** A column's GraphQL name: postgraphile camelCases the snake_case rindexer writes (`F` → `f`). */
50
+ 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) : "";
53
+ if (prefix === null)
54
+ throw new Error(`history: no indexed contract at ${address}`);
55
+ const type = `${prefix}${event}`;
56
+ return { field: `all${plural(type)}`, type };
57
+ }
58
+ function decode(type, v) {
59
+ if (v === null || v === undefined)
60
+ return v;
61
+ if (type.endsWith("[]"))
62
+ return v.map((x) => decode(type.slice(0, -2), x));
63
+ if (type.startsWith("uint") || type.startsWith("int"))
64
+ return BigInt(v);
65
+ if (type === "address")
66
+ return getAddress(v);
67
+ return v; // bool, bytes32 (hex string), string
68
+ }
69
+ const PAGE = 1000;
70
+ const MAX_PAGES = 200;
71
+ async function fromIndexer(c, q) {
72
+ const { field, type } = tableFor(c, q.address, q.event.name);
73
+ const inputs = q.event.inputs;
74
+ const condition = {};
75
+ const filter = {};
76
+ for (const [k, v] of Object.entries(q.args ?? {})) {
77
+ const input = inputs.find((i) => i.name === k);
78
+ if (!input || v === undefined || v === null)
79
+ continue;
80
+ const enc = (x) => (input.type === "bytes32" ? bytea(x) : typeof x === "string" ? x.toLowerCase() : String(x));
81
+ if (Array.isArray(v)) {
82
+ // postgraphile's filter cannot match a bytea column; an OR over one leaves this to the chain
83
+ if (input.type === "bytes32")
84
+ throw new Error("history: OR over a bytes32 argument");
85
+ filter[column(k)] = { in: v.map(enc) };
86
+ }
87
+ else {
88
+ condition[column(k)] = enc(v);
89
+ }
90
+ }
91
+ const range = {};
92
+ if (q.fromBlock !== undefined)
93
+ range.greaterThanOrEqualTo = q.fromBlock.toString();
94
+ if (q.toBlock !== undefined)
95
+ range.lessThanOrEqualTo = q.toBlock.toString();
96
+ if (Object.keys(range).length)
97
+ filter.blockNumber = range;
98
+ if (q.since !== undefined)
99
+ filter.blockTimestamp = { greaterThanOrEqualTo: new Date(q.since * 1000).toISOString() };
100
+ const selection = [...inputs.map((i) => column(i.name)), "contractAddress", "blockNumber", "blockTimestamp", "txHash", "logIndex"].join(" ");
101
+ const query = `query History($after: Cursor, $condition: ${type}Condition, $filter: ${type}Filter) {
102
+ ${field}(first: ${PAGE}, after: $after, orderBy: PRIMARY_KEY_ASC, condition: $condition, filter: $filter) {
103
+ pageInfo { hasNextPage endCursor } nodes { ${selection} }
104
+ }
105
+ }`;
106
+ const rows = [];
107
+ let after = null;
108
+ for (let i = 0;; i++) {
109
+ if (i === MAX_PAGES)
110
+ throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
111
+ const d = await gql(q.indexer, query, { after, condition: Object.keys(condition).length ? condition : null, filter: Object.keys(filter).length ? filter : null }, q.signal);
112
+ const page = d[field];
113
+ rows.push(...page.nodes);
114
+ if (!page.pageInfo.hasNextPage || !page.pageInfo.endCursor)
115
+ break;
116
+ after = page.pageInfo.endCursor;
117
+ }
118
+ // a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
119
+ const want = q.address.toLowerCase();
120
+ return rows
121
+ .filter((r) => String(r.contractAddress).toLowerCase() === want)
122
+ .map((r) => ({
123
+ address: q.address,
124
+ args: Object.fromEntries(inputs.map((i) => [i.name, decode(i.type, r[column(i.name)])])),
125
+ blockNumber: BigInt(r.blockNumber),
126
+ blockTimestamp: Math.floor(Date.parse(r.blockTimestamp) / 1000),
127
+ transactionHash: r.txHash,
128
+ logIndex: Number(r.logIndex),
129
+ }))
130
+ .sort((a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1));
131
+ }
132
+ // ─── the chain path ──────────────────────────────────────────────────────────
133
+ async function fromChain(c, q) {
134
+ let fromBlock = q.fromBlock ?? 0n;
135
+ const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
136
+ if (q.since !== undefined)
137
+ fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
138
+ const logs = await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock });
139
+ return logs.map((l) => ({
140
+ address: q.address,
141
+ args: l.args,
142
+ blockNumber: l.blockNumber,
143
+ transactionHash: l.transactionHash,
144
+ logIndex: l.logIndex,
145
+ }));
146
+ }
package/dist/index.d.ts CHANGED
@@ -20,5 +20,6 @@ export * from "./indexer.js";
20
20
  export * from "./tokencache.js";
21
21
  export * from "./fpool.js";
22
22
  export * from "./fees.js";
23
+ export * from "./history.js";
23
24
  export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
24
25
  export * from "./admin.js";
package/dist/index.js CHANGED
@@ -20,5 +20,6 @@ export * from "./indexer.js";
20
20
  export * from "./tokencache.js";
21
21
  export * from "./fpool.js";
22
22
  export * from "./fees.js";
23
+ export * from "./history.js";
23
24
  export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
24
25
  export * from "./admin.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logswap/sdk",
3
- "version": "0.5.60",
3
+ "version": "0.5.61",
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/history.ts ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * history.ts — event history from the indexer, shaped like viem's `getLogs`, with the chain as the
3
+ * fallback.
4
+ *
5
+ * Every history read used to be a node `getLogs` from a pool's birth (or block 0) to the head: a
6
+ * full re-download on every page load, seconds each against a distant node, and silently partial
7
+ * once the node pruned its history. rindexer holds the same events with their block timestamps, so
8
+ * a window is a filter rather than a block search and a scan is one paged query.
9
+ *
10
+ * `getEventLogs` takes the ABI event exactly as `getLogs` does and derives the rest from it: the
11
+ * table (`all<Name>s`, or the manager-prefixed name where two contracts share an event name, as the
12
+ * ERC-6909 `Transfer` does), the columns (the inputs' names, camelCased as postgraphile does), and
13
+ * the decoding (the inputs' types — numbers as bigint, addresses checksummed, arrays from JSON).
14
+ * With no indexer URL, or when the query fails, it asks the node and says so in `source`.
15
+ */
16
+ import { getAddress, type AbiEvent, type Address, type Hex } from "viem";
17
+ import type { LogswapClient } from "./client.js";
18
+ import { bytea, gql } from "./indexer.js";
19
+ import { blockAtOrAfter } from "./fees.js";
20
+
21
+ /** One event, as `getLogs` returns it plus its block's timestamp (unix seconds). */
22
+ export interface HistoryLog<A = Record<string, unknown>> {
23
+ address: Address;
24
+ args: A;
25
+ blockNumber: bigint;
26
+ /** Unix seconds. Always set from the indexer; set on the chain path only when a window asked for it. */
27
+ blockTimestamp?: number;
28
+ transactionHash: Hex;
29
+ logIndex: number;
30
+ }
31
+
32
+ export interface EventLogsQuery {
33
+ /** The rindexer GraphQL endpoint; absent means the chain. */
34
+ indexer?: string;
35
+ address: Address;
36
+ event: AbiEvent;
37
+ /** Indexed arguments to match, as `getLogs` takes them; an array matches any of its values. */
38
+ args?: Record<string, unknown>;
39
+ fromBlock?: bigint;
40
+ toBlock?: bigint;
41
+ /** Unix seconds: only events at or after this time. On the chain path it becomes a block search. */
42
+ since?: number;
43
+ signal?: AbortSignal;
44
+ }
45
+
46
+ /**
47
+ * The events, oldest first, and where they came from. `"indexer"` rows lag the head by the
48
+ * indexer's own lag — a few blocks at most on the devnet (logswap-health checks it).
49
+ */
50
+ export async function getEventLogs<A = Record<string, unknown>>(
51
+ c: LogswapClient,
52
+ q: EventLogsQuery,
53
+ ): Promise<{ logs: HistoryLog<A>[]; source: "indexer" | "chain" }> {
54
+ if (q.indexer) {
55
+ try {
56
+ return { logs: await fromIndexer<A>(c, q), source: "indexer" };
57
+ } catch {
58
+ /* the indexer is a cache: when it cannot answer, the node can */
59
+ }
60
+ }
61
+ return { logs: await fromChain<A>(c, q), source: "chain" };
62
+ }
63
+
64
+ // ─── the indexer path ────────────────────────────────────────────────────────
65
+
66
+ /** What the rindexer deployment calls each manager — the prefix of a table two contracts share. */
67
+ function contractName(c: LogswapClient, address: Address): string | null {
68
+ const a = address.toLowerCase();
69
+ if (a === c.addresses.cPoolManager.toLowerCase()) return "LogswapManager";
70
+ if (c.addresses.fPoolManager && a === c.addresses.fPoolManager.toLowerCase()) return "FPoolManager";
71
+ if (a === c.addresses.router.toLowerCase()) return "LogswapRouter";
72
+ return null;
73
+ }
74
+
75
+ /** Events whose name two indexed contracts share: postgraphile prefixes their tables. */
76
+ const SHARED = new Set(["Transfer"]);
77
+
78
+ const plural = (n: string) => (/(s|x|ch|sh)$/.test(n) ? `${n}es` : `${n}s`);
79
+ /** A column's GraphQL name: postgraphile camelCases the snake_case rindexer writes (`F` → `f`). */
80
+ const column = (name: string) => name[0]!.toLowerCase() + name.slice(1);
81
+
82
+ export function tableFor(c: LogswapClient, address: Address, event: string): { field: string; type: string } {
83
+ const prefix = SHARED.has(event) ? contractName(c, address) : "";
84
+ if (prefix === null) throw new Error(`history: no indexed contract at ${address}`);
85
+ const type = `${prefix}${event}`;
86
+ return { field: `all${plural(type)}`, type };
87
+ }
88
+
89
+ function decode(type: string, v: unknown): unknown {
90
+ if (v === null || v === undefined) return v;
91
+ if (type.endsWith("[]")) return (v as unknown[]).map((x) => decode(type.slice(0, -2), x));
92
+ if (type.startsWith("uint") || type.startsWith("int")) return BigInt(v as string);
93
+ if (type === "address") return getAddress(v as string);
94
+ return v; // bool, bytes32 (hex string), string
95
+ }
96
+
97
+ const PAGE = 1000;
98
+ const MAX_PAGES = 200;
99
+
100
+ async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
101
+ const { field, type } = tableFor(c, q.address, q.event.name);
102
+ const inputs = q.event.inputs;
103
+ const condition: Record<string, unknown> = {};
104
+ const filter: Record<string, unknown> = {};
105
+ for (const [k, v] of Object.entries(q.args ?? {})) {
106
+ const input = inputs.find((i) => i.name === k);
107
+ if (!input || v === undefined || v === null) continue;
108
+ const enc = (x: unknown) => (input.type === "bytes32" ? bytea(x as Hex) : typeof x === "string" ? x.toLowerCase() : String(x));
109
+ if (Array.isArray(v)) {
110
+ // postgraphile's filter cannot match a bytea column; an OR over one leaves this to the chain
111
+ if (input.type === "bytes32") throw new Error("history: OR over a bytes32 argument");
112
+ filter[column(k)] = { in: v.map(enc) };
113
+ } else {
114
+ condition[column(k)] = enc(v);
115
+ }
116
+ }
117
+ const range: Record<string, string> = {};
118
+ if (q.fromBlock !== undefined) range.greaterThanOrEqualTo = q.fromBlock.toString();
119
+ if (q.toBlock !== undefined) range.lessThanOrEqualTo = q.toBlock.toString();
120
+ if (Object.keys(range).length) filter.blockNumber = range;
121
+ if (q.since !== undefined) filter.blockTimestamp = { greaterThanOrEqualTo: new Date(q.since * 1000).toISOString() };
122
+
123
+ const selection = [...inputs.map((i) => column(i.name!)), "contractAddress", "blockNumber", "blockTimestamp", "txHash", "logIndex"].join(" ");
124
+ const query = `query History($after: Cursor, $condition: ${type}Condition, $filter: ${type}Filter) {
125
+ ${field}(first: ${PAGE}, after: $after, orderBy: PRIMARY_KEY_ASC, condition: $condition, filter: $filter) {
126
+ pageInfo { hasNextPage endCursor } nodes { ${selection} }
127
+ }
128
+ }`;
129
+ type Row = Record<string, unknown>;
130
+ type Conn = { pageInfo: { hasNextPage: boolean; endCursor: string | null }; nodes: Row[] };
131
+ const rows: Row[] = [];
132
+ let after: string | null = null;
133
+ for (let i = 0; ; i++) {
134
+ if (i === MAX_PAGES) throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
135
+ const d: Record<string, Conn> = await gql<Record<string, Conn>>(
136
+ q.indexer!,
137
+ query,
138
+ { after, condition: Object.keys(condition).length ? condition : null, filter: Object.keys(filter).length ? filter : null },
139
+ q.signal,
140
+ );
141
+ const page: Conn = d[field]!;
142
+ rows.push(...page.nodes);
143
+ if (!page.pageInfo.hasNextPage || !page.pageInfo.endCursor) break;
144
+ after = page.pageInfo.endCursor;
145
+ }
146
+ // a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
147
+ const want = q.address.toLowerCase();
148
+ return rows
149
+ .filter((r) => String(r.contractAddress).toLowerCase() === want)
150
+ .map((r) => ({
151
+ address: q.address,
152
+ args: Object.fromEntries(inputs.map((i) => [i.name!, decode(i.type, r[column(i.name!)])])) as A,
153
+ blockNumber: BigInt(r.blockNumber as string),
154
+ blockTimestamp: Math.floor(Date.parse(r.blockTimestamp as string) / 1000),
155
+ transactionHash: r.txHash as Hex,
156
+ logIndex: Number(r.logIndex),
157
+ }))
158
+ .sort((a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1));
159
+ }
160
+
161
+ // ─── the chain path ──────────────────────────────────────────────────────────
162
+
163
+ async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
164
+ let fromBlock = q.fromBlock ?? 0n;
165
+ const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
166
+ if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
167
+ const logs = await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock } as never);
168
+ return (logs as unknown as Array<{ args: A; blockNumber: bigint; transactionHash: Hex; logIndex: number }>).map((l) => ({
169
+ address: q.address,
170
+ args: l.args,
171
+ blockNumber: l.blockNumber,
172
+ transactionHash: l.transactionHash,
173
+ logIndex: l.logIndex,
174
+ }));
175
+ }
package/src/index.ts CHANGED
@@ -20,5 +20,6 @@ export * from "./indexer.js";
20
20
  export * from "./tokencache.js";
21
21
  export * from "./fpool.js";
22
22
  export * from "./fees.js";
23
+ export * from "./history.js";
23
24
  export { cPoolManagerAbi, fPoolManagerAbi, logswapLensAbi, logswapRouterAbi, padPolicyAbi, polPolicyAbi, policyRegistryAbi, privatePolicyAbi, publicPolicyAbi } from "./generated.js";
24
25
  export * from "./admin.js";