@logswap/sdk 0.5.60 → 0.5.62
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/history.d.ts +61 -0
- package/dist/history.js +166 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/history.ts +205 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
/**
|
|
40
|
+
* Only the newest `latest` events (still returned oldest first) — a pool's tape, not its history.
|
|
41
|
+
* On the chain path the node is asked for the last {@link LATEST_SCAN_BLOCKS} blocks only.
|
|
42
|
+
*/
|
|
43
|
+
latest?: number;
|
|
44
|
+
/** Only events from these transactions — e.g. the `RouterCall`s behind a page of routed swaps. */
|
|
45
|
+
transactionHashes?: Hex[];
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The events, oldest first, and where they came from. `"indexer"` rows lag the head by the
|
|
50
|
+
* indexer's own lag — a few blocks at most on the devnet (logswap-health checks it).
|
|
51
|
+
*/
|
|
52
|
+
export declare function getEventLogs<A = Record<string, unknown>>(c: LogswapClient, q: EventLogsQuery): Promise<{
|
|
53
|
+
logs: HistoryLog<A>[];
|
|
54
|
+
source: "indexer" | "chain";
|
|
55
|
+
}>;
|
|
56
|
+
export declare function tableFor(c: LogswapClient, address: Address, event: string): {
|
|
57
|
+
field: string;
|
|
58
|
+
type: string;
|
|
59
|
+
};
|
|
60
|
+
/** How far back the chain path looks for a `latest` tape: a tape is recent by definition. */
|
|
61
|
+
export declare const LATEST_SCAN_BLOCKS = 5000n;
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
if (q.transactionHashes)
|
|
101
|
+
filter.txHash = { in: q.transactionHashes.map((h) => h.toLowerCase()) };
|
|
102
|
+
// a tape is one page from the head, newest first; history is every page from the start
|
|
103
|
+
const pageSize = q.latest ?? PAGE;
|
|
104
|
+
const order = q.latest !== undefined ? "[BLOCK_NUMBER_DESC, LOG_INDEX_DESC]" : "PRIMARY_KEY_ASC";
|
|
105
|
+
const selection = [...inputs.map((i) => column(i.name)), "contractAddress", "blockNumber", "blockTimestamp", "txHash", "logIndex"].join(" ");
|
|
106
|
+
// postgraphile refuses a null or an empty filter, so each argument appears only when it has content
|
|
107
|
+
const hasCondition = Object.keys(condition).length > 0;
|
|
108
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
109
|
+
const decl = `$after: Cursor${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
|
|
110
|
+
const argsOf = `first: ${pageSize}, after: $after, orderBy: ${order}${hasCondition ? ", condition: $condition" : ""}${hasFilter ? ", filter: $filter" : ""}`;
|
|
111
|
+
const query = `query History(${decl}) {
|
|
112
|
+
${field}(${argsOf}) {
|
|
113
|
+
pageInfo { hasNextPage endCursor } nodes { ${selection} }
|
|
114
|
+
}
|
|
115
|
+
}`;
|
|
116
|
+
const rows = [];
|
|
117
|
+
let after = null;
|
|
118
|
+
for (let i = 0;; i++) {
|
|
119
|
+
if (i === MAX_PAGES)
|
|
120
|
+
throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
121
|
+
const d = await gql(q.indexer, query, { after, ...(hasCondition ? { condition } : {}), ...(hasFilter ? { filter } : {}) }, q.signal);
|
|
122
|
+
const page = d[field];
|
|
123
|
+
rows.push(...page.nodes);
|
|
124
|
+
if (q.latest !== undefined || !page.pageInfo.hasNextPage || !page.pageInfo.endCursor)
|
|
125
|
+
break;
|
|
126
|
+
after = page.pageInfo.endCursor;
|
|
127
|
+
}
|
|
128
|
+
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
129
|
+
const want = q.address.toLowerCase();
|
|
130
|
+
return rows
|
|
131
|
+
.filter((r) => String(r.contractAddress).toLowerCase() === want)
|
|
132
|
+
.map((r) => ({
|
|
133
|
+
address: q.address,
|
|
134
|
+
args: Object.fromEntries(inputs.map((i) => [i.name, decode(i.type, r[column(i.name)])])),
|
|
135
|
+
blockNumber: BigInt(r.blockNumber),
|
|
136
|
+
blockTimestamp: Math.floor(Date.parse(r.blockTimestamp) / 1000),
|
|
137
|
+
transactionHash: r.txHash,
|
|
138
|
+
logIndex: Number(r.logIndex),
|
|
139
|
+
}))
|
|
140
|
+
.sort((a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1));
|
|
141
|
+
}
|
|
142
|
+
// ─── the chain path ──────────────────────────────────────────────────────────
|
|
143
|
+
/** How far back the chain path looks for a `latest` tape: a tape is recent by definition. */
|
|
144
|
+
export const LATEST_SCAN_BLOCKS = 5000n;
|
|
145
|
+
async function fromChain(c, q) {
|
|
146
|
+
let fromBlock = q.fromBlock ?? 0n;
|
|
147
|
+
const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
|
|
148
|
+
if (q.since !== undefined)
|
|
149
|
+
fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
150
|
+
if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS)
|
|
151
|
+
fromBlock = toBlock - LATEST_SCAN_BLOCKS;
|
|
152
|
+
let logs = (await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock }));
|
|
153
|
+
if (q.transactionHashes) {
|
|
154
|
+
const want = new Set(q.transactionHashes.map((h) => h.toLowerCase()));
|
|
155
|
+
logs = logs.filter((l) => want.has(l.transactionHash.toLowerCase()));
|
|
156
|
+
}
|
|
157
|
+
if (q.latest !== undefined)
|
|
158
|
+
logs = logs.slice(-q.latest);
|
|
159
|
+
return logs.map((l) => ({
|
|
160
|
+
address: q.address,
|
|
161
|
+
args: l.args,
|
|
162
|
+
blockNumber: l.blockNumber,
|
|
163
|
+
transactionHash: l.transactionHash,
|
|
164
|
+
logIndex: l.logIndex,
|
|
165
|
+
}));
|
|
166
|
+
}
|
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.
|
|
3
|
+
"version": "0.5.62",
|
|
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,205 @@
|
|
|
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
|
+
/**
|
|
44
|
+
* Only the newest `latest` events (still returned oldest first) — a pool's tape, not its history.
|
|
45
|
+
* On the chain path the node is asked for the last {@link LATEST_SCAN_BLOCKS} blocks only.
|
|
46
|
+
*/
|
|
47
|
+
latest?: number;
|
|
48
|
+
/** Only events from these transactions — e.g. the `RouterCall`s behind a page of routed swaps. */
|
|
49
|
+
transactionHashes?: Hex[];
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The events, oldest first, and where they came from. `"indexer"` rows lag the head by the
|
|
55
|
+
* indexer's own lag — a few blocks at most on the devnet (logswap-health checks it).
|
|
56
|
+
*/
|
|
57
|
+
export async function getEventLogs<A = Record<string, unknown>>(
|
|
58
|
+
c: LogswapClient,
|
|
59
|
+
q: EventLogsQuery,
|
|
60
|
+
): Promise<{ logs: HistoryLog<A>[]; source: "indexer" | "chain" }> {
|
|
61
|
+
if (q.indexer) {
|
|
62
|
+
try {
|
|
63
|
+
return { logs: await fromIndexer<A>(c, q), source: "indexer" };
|
|
64
|
+
} catch {
|
|
65
|
+
/* the indexer is a cache: when it cannot answer, the node can */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { logs: await fromChain<A>(c, q), source: "chain" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── the indexer path ────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** What the rindexer deployment calls each manager — the prefix of a table two contracts share. */
|
|
74
|
+
function contractName(c: LogswapClient, address: Address): string | null {
|
|
75
|
+
const a = address.toLowerCase();
|
|
76
|
+
if (a === c.addresses.cPoolManager.toLowerCase()) return "LogswapManager";
|
|
77
|
+
if (c.addresses.fPoolManager && a === c.addresses.fPoolManager.toLowerCase()) return "FPoolManager";
|
|
78
|
+
if (a === c.addresses.router.toLowerCase()) return "LogswapRouter";
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Events whose name two indexed contracts share: postgraphile prefixes their tables. */
|
|
83
|
+
const SHARED = new Set(["Transfer"]);
|
|
84
|
+
|
|
85
|
+
const plural = (n: string) => (/(s|x|ch|sh)$/.test(n) ? `${n}es` : `${n}s`);
|
|
86
|
+
/** A column's GraphQL name: postgraphile camelCases the snake_case rindexer writes (`F` → `f`). */
|
|
87
|
+
const column = (name: string) => name[0]!.toLowerCase() + name.slice(1);
|
|
88
|
+
|
|
89
|
+
export function tableFor(c: LogswapClient, address: Address, event: string): { field: string; type: string } {
|
|
90
|
+
const prefix = SHARED.has(event) ? contractName(c, address) : "";
|
|
91
|
+
if (prefix === null) throw new Error(`history: no indexed contract at ${address}`);
|
|
92
|
+
const type = `${prefix}${event}`;
|
|
93
|
+
return { field: `all${plural(type)}`, type };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function decode(type: string, v: unknown): unknown {
|
|
97
|
+
if (v === null || v === undefined) return v;
|
|
98
|
+
if (type.endsWith("[]")) return (v as unknown[]).map((x) => decode(type.slice(0, -2), x));
|
|
99
|
+
if (type.startsWith("uint") || type.startsWith("int")) return BigInt(v as string);
|
|
100
|
+
if (type === "address") return getAddress(v as string);
|
|
101
|
+
return v; // bool, bytes32 (hex string), string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const PAGE = 1000;
|
|
105
|
+
const MAX_PAGES = 200;
|
|
106
|
+
|
|
107
|
+
async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
108
|
+
const { field, type } = tableFor(c, q.address, q.event.name);
|
|
109
|
+
const inputs = q.event.inputs;
|
|
110
|
+
const condition: Record<string, unknown> = {};
|
|
111
|
+
const filter: Record<string, unknown> = {};
|
|
112
|
+
for (const [k, v] of Object.entries(q.args ?? {})) {
|
|
113
|
+
const input = inputs.find((i) => i.name === k);
|
|
114
|
+
if (!input || v === undefined || v === null) continue;
|
|
115
|
+
const enc = (x: unknown) => (input.type === "bytes32" ? bytea(x as Hex) : typeof x === "string" ? x.toLowerCase() : String(x));
|
|
116
|
+
if (Array.isArray(v)) {
|
|
117
|
+
// postgraphile's filter cannot match a bytea column; an OR over one leaves this to the chain
|
|
118
|
+
if (input.type === "bytes32") throw new Error("history: OR over a bytes32 argument");
|
|
119
|
+
filter[column(k)] = { in: v.map(enc) };
|
|
120
|
+
} else {
|
|
121
|
+
condition[column(k)] = enc(v);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const range: Record<string, string> = {};
|
|
125
|
+
if (q.fromBlock !== undefined) range.greaterThanOrEqualTo = q.fromBlock.toString();
|
|
126
|
+
if (q.toBlock !== undefined) range.lessThanOrEqualTo = q.toBlock.toString();
|
|
127
|
+
if (Object.keys(range).length) filter.blockNumber = range;
|
|
128
|
+
if (q.since !== undefined) filter.blockTimestamp = { greaterThanOrEqualTo: new Date(q.since * 1000).toISOString() };
|
|
129
|
+
if (q.transactionHashes) filter.txHash = { in: q.transactionHashes.map((h) => h.toLowerCase()) };
|
|
130
|
+
// a tape is one page from the head, newest first; history is every page from the start
|
|
131
|
+
const pageSize = q.latest ?? PAGE;
|
|
132
|
+
const order = q.latest !== undefined ? "[BLOCK_NUMBER_DESC, LOG_INDEX_DESC]" : "PRIMARY_KEY_ASC";
|
|
133
|
+
|
|
134
|
+
const selection = [...inputs.map((i) => column(i.name!)), "contractAddress", "blockNumber", "blockTimestamp", "txHash", "logIndex"].join(" ");
|
|
135
|
+
// postgraphile refuses a null or an empty filter, so each argument appears only when it has content
|
|
136
|
+
const hasCondition = Object.keys(condition).length > 0;
|
|
137
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
138
|
+
const decl = `$after: Cursor${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
|
|
139
|
+
const argsOf = `first: ${pageSize}, after: $after, orderBy: ${order}${hasCondition ? ", condition: $condition" : ""}${hasFilter ? ", filter: $filter" : ""}`;
|
|
140
|
+
const query = `query History(${decl}) {
|
|
141
|
+
${field}(${argsOf}) {
|
|
142
|
+
pageInfo { hasNextPage endCursor } nodes { ${selection} }
|
|
143
|
+
}
|
|
144
|
+
}`;
|
|
145
|
+
type Row = Record<string, unknown>;
|
|
146
|
+
type Conn = { pageInfo: { hasNextPage: boolean; endCursor: string | null }; nodes: Row[] };
|
|
147
|
+
const rows: Row[] = [];
|
|
148
|
+
let after: string | null = null;
|
|
149
|
+
for (let i = 0; ; i++) {
|
|
150
|
+
if (i === MAX_PAGES) throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
151
|
+
const d: Record<string, Conn> = await gql<Record<string, Conn>>(
|
|
152
|
+
q.indexer!,
|
|
153
|
+
query,
|
|
154
|
+
{ after, ...(hasCondition ? { condition } : {}), ...(hasFilter ? { filter } : {}) },
|
|
155
|
+
q.signal,
|
|
156
|
+
);
|
|
157
|
+
const page: Conn = d[field]!;
|
|
158
|
+
rows.push(...page.nodes);
|
|
159
|
+
if (q.latest !== undefined || !page.pageInfo.hasNextPage || !page.pageInfo.endCursor) break;
|
|
160
|
+
after = page.pageInfo.endCursor;
|
|
161
|
+
}
|
|
162
|
+
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
163
|
+
const want = q.address.toLowerCase();
|
|
164
|
+
return rows
|
|
165
|
+
.filter((r) => String(r.contractAddress).toLowerCase() === want)
|
|
166
|
+
.map((r) => ({
|
|
167
|
+
address: q.address,
|
|
168
|
+
args: Object.fromEntries(inputs.map((i) => [i.name!, decode(i.type, r[column(i.name!)])])) as A,
|
|
169
|
+
blockNumber: BigInt(r.blockNumber as string),
|
|
170
|
+
blockTimestamp: Math.floor(Date.parse(r.blockTimestamp as string) / 1000),
|
|
171
|
+
transactionHash: r.txHash as Hex,
|
|
172
|
+
logIndex: Number(r.logIndex),
|
|
173
|
+
}))
|
|
174
|
+
.sort((a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── the chain path ──────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** How far back the chain path looks for a `latest` tape: a tape is recent by definition. */
|
|
180
|
+
export const LATEST_SCAN_BLOCKS = 5_000n;
|
|
181
|
+
|
|
182
|
+
async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
183
|
+
let fromBlock = q.fromBlock ?? 0n;
|
|
184
|
+
const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
|
|
185
|
+
if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
186
|
+
if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS) fromBlock = toBlock - LATEST_SCAN_BLOCKS;
|
|
187
|
+
let logs = (await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock } as never)) as unknown as Array<{
|
|
188
|
+
args: A;
|
|
189
|
+
blockNumber: bigint;
|
|
190
|
+
transactionHash: Hex;
|
|
191
|
+
logIndex: number;
|
|
192
|
+
}>;
|
|
193
|
+
if (q.transactionHashes) {
|
|
194
|
+
const want = new Set(q.transactionHashes.map((h) => h.toLowerCase()));
|
|
195
|
+
logs = logs.filter((l) => want.has(l.transactionHash.toLowerCase()));
|
|
196
|
+
}
|
|
197
|
+
if (q.latest !== undefined) logs = logs.slice(-q.latest);
|
|
198
|
+
return logs.map((l) => ({
|
|
199
|
+
address: q.address,
|
|
200
|
+
args: l.args,
|
|
201
|
+
blockNumber: l.blockNumber,
|
|
202
|
+
transactionHash: l.transactionHash,
|
|
203
|
+
logIndex: l.logIndex,
|
|
204
|
+
}));
|
|
205
|
+
}
|
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";
|