@logswap/sdk 0.5.61 → 0.5.63
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/client.js +16 -19
- package/dist/fpool.d.ts +3 -1
- package/dist/fpool.js +8 -12
- package/dist/history.d.ts +9 -0
- package/dist/history.js +43 -15
- package/package.json +1 -1
- package/src/client.ts +17 -21
- package/src/fpool.ts +9 -13
- package/src/history.ts +55 -21
package/dist/client.js
CHANGED
|
@@ -123,30 +123,27 @@ export const EXPECTED_VERSION = 5n;
|
|
|
123
123
|
// `isTaker`; a pad and a POL hold one base (`OneBase`).
|
|
124
124
|
export const EXPECTED_F_VERSION = 19n;
|
|
125
125
|
export async function assertVersion(c, expected = EXPECTED_VERSION) {
|
|
126
|
-
const { cPoolManagerAbi } = await import("./generated.js");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
abi: cPoolManagerAbi,
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
const { cPoolManagerAbi, fPoolManagerAbi } = await import("./generated.js");
|
|
127
|
+
// three independent reads: one round trip, not three in a row
|
|
128
|
+
const [got, gotF, wiring] = await Promise.all([
|
|
129
|
+
c.public.readContract({ address: c.addresses.cPoolManager, abi: cPoolManagerAbi, functionName: "version" }),
|
|
130
|
+
// the F manager carries its own tag; assert it the same way when deployed
|
|
131
|
+
c.addresses.fPoolManager
|
|
132
|
+
? c.public.readContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName: "version" })
|
|
133
|
+
: Promise.resolve(null),
|
|
134
|
+
assertWiring(c).then(() => null, (e) => e),
|
|
135
|
+
]);
|
|
132
136
|
if (got !== expected) {
|
|
133
137
|
throw new Error(`logswap: manager at ${c.addresses.cPoolManager} reports version ${got}, but this SDK was built ` +
|
|
134
138
|
`against version ${expected}. Upgrade the SDK or point at the matching deployment.`);
|
|
135
139
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const gotF = await c.public.readContract({
|
|
140
|
-
address: c.addresses.fPoolManager,
|
|
141
|
-
abi: fPoolManagerAbi,
|
|
142
|
-
functionName: "version",
|
|
143
|
-
});
|
|
144
|
-
if (gotF < EXPECTED_F_VERSION) {
|
|
145
|
-
throw new Error(`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
146
|
-
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`);
|
|
147
|
-
}
|
|
140
|
+
if (gotF !== null && gotF < EXPECTED_F_VERSION) {
|
|
141
|
+
throw new Error(`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
142
|
+
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`);
|
|
148
143
|
}
|
|
149
|
-
|
|
144
|
+
// the wiring's error is the less specific one, so a version mismatch is reported first
|
|
145
|
+
if (wiring)
|
|
146
|
+
throw wiring;
|
|
150
147
|
}
|
|
151
148
|
/**
|
|
152
149
|
* The versions say the managers are the right code; this says the rest of the addresses belong
|
package/dist/fpool.d.ts
CHANGED
|
@@ -1433,11 +1433,13 @@ export interface DiscoveredFPool {
|
|
|
1433
1433
|
* Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
|
|
1434
1434
|
* policy, cached. Possible only because the event carries the full key preimage (bases,
|
|
1435
1435
|
* weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
|
|
1436
|
-
* exists for exactly this call.
|
|
1436
|
+
* exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
|
|
1437
|
+
* a scan from block 0 per call is what made the first load wait on the node.
|
|
1437
1438
|
*/
|
|
1438
1439
|
export declare function discoverFPools(c: LogswapClient, opts?: {
|
|
1439
1440
|
fromBlock?: bigint;
|
|
1440
1441
|
toBlock?: bigint;
|
|
1442
|
+
indexer?: string;
|
|
1441
1443
|
}): Promise<DiscoveredFPool[]>;
|
|
1442
1444
|
/** A pool an authority controls, with what the policy page shows of it. */
|
|
1443
1445
|
export interface FPoolControlled {
|
package/dist/fpool.js
CHANGED
|
@@ -46,6 +46,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
46
46
|
*/
|
|
47
47
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
48
48
|
import { fallBackUnlessRejected, signerFor } from "./write.js";
|
|
49
|
+
import { getEventLogs } from "./history.js";
|
|
49
50
|
const WAD = 10n ** 18n;
|
|
50
51
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
51
52
|
/** The optional questions a policy declared at birth, as bits of `Pool.asks`. */
|
|
@@ -1044,23 +1045,18 @@ export async function fPoolTransferShares(c, a) {
|
|
|
1044
1045
|
* Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
|
|
1045
1046
|
* policy, cached. Possible only because the event carries the full key preimage (bases,
|
|
1046
1047
|
* weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
|
|
1047
|
-
* exists for exactly this call.
|
|
1048
|
+
* exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
|
|
1049
|
+
* a scan from block 0 per call is what made the first load wait on the node.
|
|
1048
1050
|
*/
|
|
1049
1051
|
export async function discoverFPools(c, opts = {}) {
|
|
1050
1052
|
const ev = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "FInitialize");
|
|
1051
1053
|
if (!ev)
|
|
1052
1054
|
throw new Error("logswap: F Initialize event missing from the generated ABI");
|
|
1053
1055
|
const seedEv = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "Seeded");
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
fromBlock: opts.fromBlock ?? 0n,
|
|
1059
|
-
toBlock: opts.toBlock ?? "latest",
|
|
1060
|
-
}),
|
|
1061
|
-
seedEv
|
|
1062
|
-
? c.public.getLogs({ address: c.addresses.fPoolManager, event: seedEv, fromBlock: opts.fromBlock ?? 0n, toBlock: opts.toBlock ?? "latest" })
|
|
1063
|
-
: Promise.resolve([]),
|
|
1056
|
+
const range = { indexer: opts.indexer, address: c.addresses.fPoolManager, fromBlock: opts.fromBlock, toBlock: opts.toBlock };
|
|
1057
|
+
const [{ logs }, { logs: seeds }] = await Promise.all([
|
|
1058
|
+
getEventLogs(c, { ...range, event: ev }),
|
|
1059
|
+
seedEv ? getEventLogs(c, { ...range, event: seedEv }) : Promise.resolve({ logs: [] }),
|
|
1064
1060
|
]);
|
|
1065
1061
|
const q0Of = new Map();
|
|
1066
1062
|
for (const l of seeds) {
|
|
@@ -1080,7 +1076,7 @@ export async function discoverFPools(c, opts = {}) {
|
|
|
1080
1076
|
kind,
|
|
1081
1077
|
policy: a.policy,
|
|
1082
1078
|
creator: a.creator,
|
|
1083
|
-
block: l.blockNumber
|
|
1079
|
+
block: l.blockNumber,
|
|
1084
1080
|
seeded: q0 !== undefined,
|
|
1085
1081
|
q0: q0 ?? 0n,
|
|
1086
1082
|
shape: kind === "pad" ? "launch" : "basket", // the policy's word, not n = 1: a mono POL is a pool
|
package/dist/history.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ export interface EventLogsQuery {
|
|
|
36
36
|
toBlock?: bigint;
|
|
37
37
|
/** Unix seconds: only events at or after this time. On the chain path it becomes a block search. */
|
|
38
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[];
|
|
39
46
|
signal?: AbortSignal;
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
@@ -50,3 +57,5 @@ export declare function tableFor(c: LogswapClient, address: Address, event: stri
|
|
|
50
57
|
field: string;
|
|
51
58
|
type: string;
|
|
52
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
CHANGED
|
@@ -66,8 +66,10 @@ function decode(type, v) {
|
|
|
66
66
|
return getAddress(v);
|
|
67
67
|
return v; // bool, bytes32 (hex string), string
|
|
68
68
|
}
|
|
69
|
-
const PAGE = 1000;
|
|
69
|
+
const PAGE = 1000; // postgraphile's cap
|
|
70
70
|
const MAX_PAGES = 200;
|
|
71
|
+
/** Pages in flight at once, after the first. */
|
|
72
|
+
const PARALLEL_PAGES = 8;
|
|
71
73
|
async function fromIndexer(c, q) {
|
|
72
74
|
const { field, type } = tableFor(c, q.address, q.event.name);
|
|
73
75
|
const inputs = q.event.inputs;
|
|
@@ -97,23 +99,39 @@ async function fromIndexer(c, q) {
|
|
|
97
99
|
filter.blockNumber = range;
|
|
98
100
|
if (q.since !== undefined)
|
|
99
101
|
filter.blockTimestamp = { greaterThanOrEqualTo: new Date(q.since * 1000).toISOString() };
|
|
102
|
+
if (q.transactionHashes)
|
|
103
|
+
filter.txHash = { in: q.transactionHashes.map((h) => h.toLowerCase()) };
|
|
104
|
+
// a tape is one page from the head, newest first; history is every page from the start
|
|
105
|
+
const pageSize = q.latest ?? PAGE;
|
|
106
|
+
const order = q.latest !== undefined ? "[BLOCK_NUMBER_DESC, LOG_INDEX_DESC]" : "PRIMARY_KEY_ASC";
|
|
100
107
|
const selection = [...inputs.map((i) => column(i.name)), "contractAddress", "blockNumber", "blockTimestamp", "txHash", "logIndex"].join(" ");
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
// postgraphile refuses a null or an empty filter, so each argument appears only when it has content
|
|
109
|
+
const hasCondition = Object.keys(condition).length > 0;
|
|
110
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
111
|
+
const decl = `$offset: Int${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
|
|
112
|
+
const argsOf = `first: ${pageSize}, offset: $offset, orderBy: ${order}${hasCondition ? ", condition: $condition" : ""}${hasFilter ? ", filter: $filter" : ""}`;
|
|
113
|
+
const query = `query History(${decl}) {
|
|
114
|
+
${field}(${argsOf}) {
|
|
115
|
+
totalCount nodes { ${selection} }
|
|
104
116
|
}
|
|
105
117
|
}`;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
const page = async (offset) => (await gql(q.indexer, query, { offset, ...(hasCondition ? { condition } : {}), ...(hasFilter ? { filter } : {}) }, q.signal))[field];
|
|
119
|
+
// The first page says how many rows there are; the rest are fetched in parallel by offset, not
|
|
120
|
+
// one cursor after another: a day of a busy pool is a few pages, and a round trip to a distant
|
|
121
|
+
// indexer costs more than the page. Ascending by key, rows that land meanwhile append at the end
|
|
122
|
+
// and shift no earlier offset.
|
|
123
|
+
const first = await page(0);
|
|
124
|
+
const rows = [...first.nodes];
|
|
125
|
+
if (q.latest === undefined && first.totalCount > first.nodes.length) {
|
|
126
|
+
const pages = Math.ceil(first.totalCount / PAGE);
|
|
127
|
+
if (pages > MAX_PAGES)
|
|
110
128
|
throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
const offsets = Array.from({ length: pages - 1 }, (_, i) => (i + 1) * PAGE);
|
|
130
|
+
for (let i = 0; i < offsets.length; i += PARALLEL_PAGES) {
|
|
131
|
+
const got = await Promise.all(offsets.slice(i, i + PARALLEL_PAGES).map(page));
|
|
132
|
+
for (const g of got)
|
|
133
|
+
rows.push(...g.nodes);
|
|
134
|
+
}
|
|
117
135
|
}
|
|
118
136
|
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
119
137
|
const want = q.address.toLowerCase();
|
|
@@ -130,12 +148,22 @@ async function fromIndexer(c, q) {
|
|
|
130
148
|
.sort((a, b) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1));
|
|
131
149
|
}
|
|
132
150
|
// ─── the chain path ──────────────────────────────────────────────────────────
|
|
151
|
+
/** How far back the chain path looks for a `latest` tape: a tape is recent by definition. */
|
|
152
|
+
export const LATEST_SCAN_BLOCKS = 5000n;
|
|
133
153
|
async function fromChain(c, q) {
|
|
134
154
|
let fromBlock = q.fromBlock ?? 0n;
|
|
135
155
|
const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
|
|
136
156
|
if (q.since !== undefined)
|
|
137
157
|
fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
138
|
-
|
|
158
|
+
if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS)
|
|
159
|
+
fromBlock = toBlock - LATEST_SCAN_BLOCKS;
|
|
160
|
+
let logs = (await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock }));
|
|
161
|
+
if (q.transactionHashes) {
|
|
162
|
+
const want = new Set(q.transactionHashes.map((h) => h.toLowerCase()));
|
|
163
|
+
logs = logs.filter((l) => want.has(l.transactionHash.toLowerCase()));
|
|
164
|
+
}
|
|
165
|
+
if (q.latest !== undefined)
|
|
166
|
+
logs = logs.slice(-q.latest);
|
|
139
167
|
return logs.map((l) => ({
|
|
140
168
|
address: q.address,
|
|
141
169
|
args: l.args,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logswap/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.63",
|
|
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/client.ts
CHANGED
|
@@ -168,34 +168,30 @@ export const EXPECTED_VERSION = 5n;
|
|
|
168
168
|
export const EXPECTED_F_VERSION = 19n;
|
|
169
169
|
|
|
170
170
|
export async function assertVersion(c: LogswapClient, expected = EXPECTED_VERSION): Promise<void> {
|
|
171
|
-
const { cPoolManagerAbi } = await import("./generated.js");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
abi: cPoolManagerAbi,
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
const { cPoolManagerAbi, fPoolManagerAbi } = await import("./generated.js");
|
|
172
|
+
// three independent reads: one round trip, not three in a row
|
|
173
|
+
const [got, gotF, wiring] = await Promise.all([
|
|
174
|
+
c.public.readContract({ address: c.addresses.cPoolManager, abi: cPoolManagerAbi, functionName: "version" }),
|
|
175
|
+
// the F manager carries its own tag; assert it the same way when deployed
|
|
176
|
+
c.addresses.fPoolManager
|
|
177
|
+
? c.public.readContract({ address: c.addresses.fPoolManager, abi: fPoolManagerAbi, functionName: "version" })
|
|
178
|
+
: Promise.resolve(null),
|
|
179
|
+
assertWiring(c).then(() => null, (e: unknown) => e),
|
|
180
|
+
]);
|
|
177
181
|
if (got !== expected) {
|
|
178
182
|
throw new Error(
|
|
179
183
|
`logswap: manager at ${c.addresses.cPoolManager} reports version ${got}, but this SDK was built ` +
|
|
180
184
|
`against version ${expected}. Upgrade the SDK or point at the matching deployment.`,
|
|
181
185
|
);
|
|
182
186
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
abi: fPoolManagerAbi,
|
|
189
|
-
functionName: "version",
|
|
190
|
-
});
|
|
191
|
-
if (gotF < EXPECTED_F_VERSION) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
194
|
-
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
187
|
+
if (gotF !== null && gotF < EXPECTED_F_VERSION) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`logswap: F manager at ${c.addresses.fPoolManager} reports version ${gotF}, but this SDK needs ` +
|
|
190
|
+
`at least F version ${EXPECTED_F_VERSION}. Upgrade the deployment or point at a newer one.`,
|
|
191
|
+
);
|
|
197
192
|
}
|
|
198
|
-
|
|
193
|
+
// the wiring's error is the less specific one, so a version mismatch is reported first
|
|
194
|
+
if (wiring) throw wiring;
|
|
199
195
|
}
|
|
200
196
|
|
|
201
197
|
/**
|
package/src/fpool.ts
CHANGED
|
@@ -49,6 +49,7 @@ export { fPoolManagerAbi, padPolicyAbi, polPolicyAbi, privatePolicyAbi, publicPo
|
|
|
49
49
|
export const fPolicyBaseAbi = polPolicyAbi;
|
|
50
50
|
import type { LogswapClient } from "./client.js";
|
|
51
51
|
import { fallBackUnlessRejected, signerFor } from "./write.js";
|
|
52
|
+
import { getEventLogs } from "./history.js";
|
|
52
53
|
|
|
53
54
|
const WAD = 10n ** 18n;
|
|
54
55
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" as const;
|
|
@@ -1474,27 +1475,22 @@ export interface DiscoveredFPool {
|
|
|
1474
1475
|
* Every F pool ever created, from `Initialize` logs alone — plus one `kind()` read per distinct
|
|
1475
1476
|
* policy, cached. Possible only because the event carries the full key preimage (bases,
|
|
1476
1477
|
* weights, phi, policy) — the poolId is a hash and could never be inverted; contracts PR #17
|
|
1477
|
-
* exists for exactly this call.
|
|
1478
|
+
* exists for exactly this call. With `indexer`, the logs come from it (the node when it fails):
|
|
1479
|
+
* a scan from block 0 per call is what made the first load wait on the node.
|
|
1478
1480
|
*/
|
|
1479
1481
|
export async function discoverFPools(
|
|
1480
1482
|
c: LogswapClient,
|
|
1481
|
-
opts: { fromBlock?: bigint; toBlock?: bigint } = {},
|
|
1483
|
+
opts: { fromBlock?: bigint; toBlock?: bigint; indexer?: string } = {},
|
|
1482
1484
|
): Promise<DiscoveredFPool[]> {
|
|
1483
1485
|
type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "FInitialize" }>;
|
|
1484
1486
|
const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "FInitialize");
|
|
1485
1487
|
if (!ev) throw new Error("logswap: F Initialize event missing from the generated ABI");
|
|
1486
1488
|
type SeedEv = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "Seeded" }>;
|
|
1487
1489
|
const seedEv = fPoolManagerAbi.find((x): x is SeedEv => x.type === "event" && x.name === "Seeded");
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
fromBlock: opts.fromBlock ?? 0n,
|
|
1493
|
-
toBlock: opts.toBlock ?? "latest",
|
|
1494
|
-
}),
|
|
1495
|
-
seedEv
|
|
1496
|
-
? c.public.getLogs({ address: c.addresses.fPoolManager!, event: seedEv, fromBlock: opts.fromBlock ?? 0n, toBlock: opts.toBlock ?? "latest" })
|
|
1497
|
-
: Promise.resolve([]),
|
|
1490
|
+
const range = { indexer: opts.indexer, address: c.addresses.fPoolManager!, fromBlock: opts.fromBlock, toBlock: opts.toBlock };
|
|
1491
|
+
const [{ logs }, { logs: seeds }] = await Promise.all([
|
|
1492
|
+
getEventLogs(c, { ...range, event: ev }),
|
|
1493
|
+
seedEv ? getEventLogs(c, { ...range, event: seedEv }) : Promise.resolve({ logs: [] }),
|
|
1498
1494
|
]);
|
|
1499
1495
|
const q0Of = new Map<string, bigint>();
|
|
1500
1496
|
for (const l of seeds) {
|
|
@@ -1518,7 +1514,7 @@ export async function discoverFPools(
|
|
|
1518
1514
|
kind,
|
|
1519
1515
|
policy: a.policy,
|
|
1520
1516
|
creator: a.creator,
|
|
1521
|
-
block: l.blockNumber
|
|
1517
|
+
block: l.blockNumber,
|
|
1522
1518
|
seeded: q0 !== undefined,
|
|
1523
1519
|
q0: q0 ?? 0n,
|
|
1524
1520
|
shape: kind === "pad" ? "launch": "basket", // the policy's word, not n = 1: a mono POL is a pool
|
package/src/history.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface EventLogsQuery {
|
|
|
40
40
|
toBlock?: bigint;
|
|
41
41
|
/** Unix seconds: only events at or after this time. On the chain path it becomes a block search. */
|
|
42
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[];
|
|
43
50
|
signal?: AbortSignal;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -94,8 +101,10 @@ function decode(type: string, v: unknown): unknown {
|
|
|
94
101
|
return v; // bool, bytes32 (hex string), string
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
const PAGE = 1000;
|
|
104
|
+
const PAGE = 1000; // postgraphile's cap
|
|
98
105
|
const MAX_PAGES = 200;
|
|
106
|
+
/** Pages in flight at once, after the first. */
|
|
107
|
+
const PARALLEL_PAGES = 8;
|
|
99
108
|
|
|
100
109
|
async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
101
110
|
const { field, type } = tableFor(c, q.address, q.event.name);
|
|
@@ -119,29 +128,40 @@ async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<Hist
|
|
|
119
128
|
if (q.toBlock !== undefined) range.lessThanOrEqualTo = q.toBlock.toString();
|
|
120
129
|
if (Object.keys(range).length) filter.blockNumber = range;
|
|
121
130
|
if (q.since !== undefined) filter.blockTimestamp = { greaterThanOrEqualTo: new Date(q.since * 1000).toISOString() };
|
|
131
|
+
if (q.transactionHashes) filter.txHash = { in: q.transactionHashes.map((h) => h.toLowerCase()) };
|
|
132
|
+
// a tape is one page from the head, newest first; history is every page from the start
|
|
133
|
+
const pageSize = q.latest ?? PAGE;
|
|
134
|
+
const order = q.latest !== undefined ? "[BLOCK_NUMBER_DESC, LOG_INDEX_DESC]" : "PRIMARY_KEY_ASC";
|
|
122
135
|
|
|
123
136
|
const selection = [...inputs.map((i) => column(i.name!)), "contractAddress", "blockNumber", "blockTimestamp", "txHash", "logIndex"].join(" ");
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
// postgraphile refuses a null or an empty filter, so each argument appears only when it has content
|
|
138
|
+
const hasCondition = Object.keys(condition).length > 0;
|
|
139
|
+
const hasFilter = Object.keys(filter).length > 0;
|
|
140
|
+
const decl = `$offset: Int${hasCondition ? `, $condition: ${type}Condition` : ""}${hasFilter ? `, $filter: ${type}Filter` : ""}`;
|
|
141
|
+
const argsOf = `first: ${pageSize}, offset: $offset, orderBy: ${order}${hasCondition ? ", condition: $condition" : ""}${hasFilter ? ", filter: $filter" : ""}`;
|
|
142
|
+
const query = `query History(${decl}) {
|
|
143
|
+
${field}(${argsOf}) {
|
|
144
|
+
totalCount nodes { ${selection} }
|
|
127
145
|
}
|
|
128
146
|
}`;
|
|
129
147
|
type Row = Record<string, unknown>;
|
|
130
|
-
type Conn = {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
type Conn = { totalCount: number; nodes: Row[] };
|
|
149
|
+
const page = async (offset: number): Promise<Conn> =>
|
|
150
|
+
(await gql<Record<string, Conn>>(q.indexer!, query, { offset, ...(hasCondition ? { condition } : {}), ...(hasFilter ? { filter } : {}) }, q.signal))[field]!;
|
|
151
|
+
// The first page says how many rows there are; the rest are fetched in parallel by offset, not
|
|
152
|
+
// one cursor after another: a day of a busy pool is a few pages, and a round trip to a distant
|
|
153
|
+
// indexer costs more than the page. Ascending by key, rows that land meanwhile append at the end
|
|
154
|
+
// and shift no earlier offset.
|
|
155
|
+
const first = await page(0);
|
|
156
|
+
const rows: Row[] = [...first.nodes];
|
|
157
|
+
if (q.latest === undefined && first.totalCount > first.nodes.length) {
|
|
158
|
+
const pages = Math.ceil(first.totalCount / PAGE);
|
|
159
|
+
if (pages > MAX_PAGES) throw new Error(`history: ${field} has more than ${PAGE * MAX_PAGES} rows`);
|
|
160
|
+
const offsets = Array.from({ length: pages - 1 }, (_, i) => (i + 1) * PAGE);
|
|
161
|
+
for (let i = 0; i < offsets.length; i += PARALLEL_PAGES) {
|
|
162
|
+
const got = await Promise.all(offsets.slice(i, i + PARALLEL_PAGES).map(page));
|
|
163
|
+
for (const g of got) rows.push(...g.nodes);
|
|
164
|
+
}
|
|
145
165
|
}
|
|
146
166
|
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
147
167
|
const want = q.address.toLowerCase();
|
|
@@ -160,12 +180,26 @@ async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<Hist
|
|
|
160
180
|
|
|
161
181
|
// ─── the chain path ──────────────────────────────────────────────────────────
|
|
162
182
|
|
|
183
|
+
/** How far back the chain path looks for a `latest` tape: a tape is recent by definition. */
|
|
184
|
+
export const LATEST_SCAN_BLOCKS = 5_000n;
|
|
185
|
+
|
|
163
186
|
async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
164
187
|
let fromBlock = q.fromBlock ?? 0n;
|
|
165
188
|
const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
|
|
166
189
|
if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS) fromBlock = toBlock - LATEST_SCAN_BLOCKS;
|
|
191
|
+
let logs = (await c.public.getLogs({ address: q.address, event: q.event, args: q.args, fromBlock, toBlock } as never)) as unknown as Array<{
|
|
192
|
+
args: A;
|
|
193
|
+
blockNumber: bigint;
|
|
194
|
+
transactionHash: Hex;
|
|
195
|
+
logIndex: number;
|
|
196
|
+
}>;
|
|
197
|
+
if (q.transactionHashes) {
|
|
198
|
+
const want = new Set(q.transactionHashes.map((h) => h.toLowerCase()));
|
|
199
|
+
logs = logs.filter((l) => want.has(l.transactionHash.toLowerCase()));
|
|
200
|
+
}
|
|
201
|
+
if (q.latest !== undefined) logs = logs.slice(-q.latest);
|
|
202
|
+
return logs.map((l) => ({
|
|
169
203
|
address: q.address,
|
|
170
204
|
args: l.args,
|
|
171
205
|
blockNumber: l.blockNumber,
|