@logswap/sdk 0.5.74 → 0.5.75
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 +15 -0
- package/dist/client.d.ts +2 -0
- package/dist/fees.d.ts +2 -1
- package/dist/fees.js +2 -1
- package/dist/fpool.d.ts +2 -1
- package/dist/fpool.js +1 -7
- package/dist/history.d.ts +82 -2
- package/dist/history.js +108 -16
- package/dist/pools.d.ts +2 -1
- package/dist/pools.js +10 -3
- package/dist/positions.d.ts +3 -1
- package/dist/positions.js +3 -2
- package/package.json +1 -1
- package/src/client.ts +2 -0
- package/src/fees.ts +4 -3
- package/src/fpool.ts +3 -9
- package/src/history.ts +150 -17
- package/src/pools.ts +16 -11
- package/src/positions.ts +4 -3
package/README.md
CHANGED
|
@@ -69,6 +69,21 @@ 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
|
+
|
|
72
87
|
## Capped F pools and the lien (F version 25)
|
|
73
88
|
|
|
74
89
|
- **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 Swap rows from there, 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,7 @@
|
|
|
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";
|
|
14
15
|
const WAD = 10n ** 18n;
|
|
15
16
|
const YEAR_SECONDS = 31_536_000;
|
|
16
17
|
/**
|
|
@@ -89,7 +90,7 @@ export async function swapFees(c, m, opts = {}) {
|
|
|
89
90
|
throw new Error("logswap: Swap event missing from the generated ABI");
|
|
90
91
|
const logs = fromBlock > toBlock
|
|
91
92
|
? []
|
|
92
|
-
: await c
|
|
93
|
+
: (await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock })).logs;
|
|
93
94
|
// the event carries the fee the swap charged (decisions 050), exactly — no re-derivation from φ
|
|
94
95
|
let fees = 0n;
|
|
95
96
|
for (const l of logs)
|
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 —
|
package/dist/fpool.js
CHANGED
|
@@ -779,13 +779,7 @@ export async function fPoolIdOf(c, a) {
|
|
|
779
779
|
*/
|
|
780
780
|
export async function fPoolKeyOfPool(c, poolId, opts = {}) {
|
|
781
781
|
const ev = fPoolManagerAbi.find((x) => x.type === "event" && x.name === "FInitialize");
|
|
782
|
-
const logs = await c.
|
|
783
|
-
address: c.addresses.fPoolManager,
|
|
784
|
-
event: ev,
|
|
785
|
-
args: { poolId },
|
|
786
|
-
fromBlock: opts.fromBlock ?? 0n,
|
|
787
|
-
toBlock: "latest",
|
|
788
|
-
});
|
|
782
|
+
const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.fPoolManager, event: ev, args: { poolId }, fromBlock: opts.fromBlock ?? 0n });
|
|
789
783
|
const l = logs[0];
|
|
790
784
|
if (!l)
|
|
791
785
|
throw new Error(`logswap: no FInitialize for F pool ${poolId}`);
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
36
|
-
|
|
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
|
-
|
|
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 (
|
|
46
|
+
if (is(d.fPoolManager))
|
|
41
47
|
return "FPoolManager";
|
|
42
|
-
if (
|
|
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
|
|
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 =
|
|
168
|
+
const want = new Map(all.map((a) => [a.toLowerCase(), a]));
|
|
138
169
|
return rows
|
|
139
|
-
.filter((r) => String(r.contractAddress).toLowerCase()
|
|
170
|
+
.filter((r) => want.has(String(r.contractAddress).toLowerCase()))
|
|
140
171
|
.map((r) => ({
|
|
141
|
-
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
|
-
|
|
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:
|
|
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/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
|
|
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
|
|
124
|
+
args,
|
|
118
125
|
fromBlock: opts.fromBlock ?? 0n,
|
|
119
|
-
toBlock: opts.toBlock
|
|
126
|
+
toBlock: opts.toBlock,
|
|
120
127
|
});
|
|
121
128
|
return logs.map((l) => {
|
|
122
129
|
const a = l.args;
|
package/dist/positions.d.ts
CHANGED
|
@@ -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
|
|
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,7 @@ 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";
|
|
15
16
|
/**
|
|
16
17
|
* A class's aggregate state, read through `extsload` (C version 7: the manager's public
|
|
17
18
|
* `positions` mapping is gone behind it, decisions 065). The slot is `CPoolState`'s:
|
|
@@ -191,9 +192,9 @@ const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes
|
|
|
191
192
|
* holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
|
|
192
193
|
* a read per registered class; fine for a pool's lifetime of classes.
|
|
193
194
|
*/
|
|
194
|
-
export async function holdingsOf(c, key, owner, fromBlock = 0n) {
|
|
195
|
+
export async function holdingsOf(c, key, owner, fromBlock = 0n, opts = {}) {
|
|
195
196
|
const pool = poolId(key);
|
|
196
|
-
const logs = await c
|
|
197
|
+
const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock });
|
|
197
198
|
const ids = logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
|
|
198
199
|
if (ids.length === 0)
|
|
199
200
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logswap/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.75",
|
|
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
|
@@ -37,6 +37,8 @@ export interface LogswapAddresses {
|
|
|
37
37
|
* absent; a deployment with none asks nobody.
|
|
38
38
|
*/
|
|
39
39
|
policyRegistry?: Address;
|
|
40
|
+
/** The devnet's token factory (`TokenFactory`, a mock): where launchable tokens come from. Optional. */
|
|
41
|
+
tokenFactory?: Address;
|
|
40
42
|
/** Optional — the router falls back to plain approvals when absent. */
|
|
41
43
|
permit2?: Address;
|
|
42
44
|
}
|
package/src/fees.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { LogswapClient } from "./client.js";
|
|
15
15
|
import { cPoolManagerAbi } from "./generated.js";
|
|
16
16
|
import type { DiscoveredMarket } from "./pools.js";
|
|
17
|
+
import { getEventLogs } from "./history.js";
|
|
17
18
|
|
|
18
19
|
const WAD = 10n ** 18n;
|
|
19
20
|
const YEAR_SECONDS = 31_536_000;
|
|
@@ -85,7 +86,7 @@ export async function blockAtOrAfter(c: LogswapClient, t: bigint, lo: bigint, hi
|
|
|
85
86
|
export async function swapFees(
|
|
86
87
|
c: LogswapClient,
|
|
87
88
|
m: DiscoveredMarket,
|
|
88
|
-
opts: { since?: bigint; fromBlock?: bigint; toBlock?: bigint } = {},
|
|
89
|
+
opts: { since?: bigint; fromBlock?: bigint; toBlock?: bigint; /** The rindexer GraphQL endpoint: the Swap rows from there, the node otherwise. */ indexer?: string } = {},
|
|
89
90
|
): Promise<SwapFeeWindow> {
|
|
90
91
|
const toBlock = opts.toBlock ?? (await c.public.getBlockNumber());
|
|
91
92
|
const [opened, last] = await Promise.all([
|
|
@@ -112,11 +113,11 @@ export async function swapFees(
|
|
|
112
113
|
|
|
113
114
|
const logs = fromBlock > toBlock
|
|
114
115
|
? []
|
|
115
|
-
: await c
|
|
116
|
+
: (await getEventLogs<{ fee: bigint }>(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ev, args: { poolId: m.poolId }, fromBlock, toBlock })).logs;
|
|
116
117
|
|
|
117
118
|
// the event carries the fee the swap charged (decisions 050), exactly — no re-derivation from φ
|
|
118
119
|
let fees = 0n;
|
|
119
|
-
for (const l of logs) fees += l.args.fee
|
|
120
|
+
for (const l of logs) fees += l.args.fee;
|
|
120
121
|
|
|
121
122
|
const to = last.timestamp;
|
|
122
123
|
return { fromBlock, toBlock, from, to, seconds: to > from ? to - from : 0n, swapFees: fees, swaps: logs.length };
|
package/src/fpool.ts
CHANGED
|
@@ -1150,19 +1150,13 @@ export async function fPoolIdOf(c: LogswapClient, a: Omit<FPoolCreateArgs, "acco
|
|
|
1150
1150
|
* without naming one; a pad's carries its split, so it is rebuilt from the policy's record with nonce 0 — pass `opts.salt` for another nonce). The result is checked against the id, so a wrong salt throws rather than
|
|
1151
1151
|
* returning a key that names another pool. What {@link fPoolMigrate} needs when handed an id.
|
|
1152
1152
|
*/
|
|
1153
|
-
export async function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts: { salt?: Hex; fromBlock?: bigint } = {}): Promise<FPoolKey> {
|
|
1153
|
+
export async function fPoolKeyOfPool(c: LogswapClient, poolId: Hex, opts: { salt?: Hex; fromBlock?: bigint; /** The rindexer GraphQL endpoint: `FInitialize` from there, the node otherwise. The salt is in neither (the event does not carry it). */ indexer?: string } = {}): Promise<FPoolKey> {
|
|
1154
1154
|
type Ev = Extract<(typeof fPoolManagerAbi)[number], { type: "event"; name: "FInitialize" }>;
|
|
1155
1155
|
const ev = fPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "FInitialize")!;
|
|
1156
|
-
const logs = await c.
|
|
1157
|
-
address: c.addresses.fPoolManager!,
|
|
1158
|
-
event: ev,
|
|
1159
|
-
args: { poolId },
|
|
1160
|
-
fromBlock: opts.fromBlock ?? 0n,
|
|
1161
|
-
toBlock: "latest",
|
|
1162
|
-
});
|
|
1156
|
+
const { logs } = await getEventLogs(c, { indexer: opts.indexer, address: c.addresses.fPoolManager!, event: ev as never, args: { poolId }, fromBlock: opts.fromBlock ?? 0n });
|
|
1163
1157
|
const l = logs[0];
|
|
1164
1158
|
if (!l) throw new Error(`logswap: no FInitialize for F pool ${poolId}`);
|
|
1165
|
-
const a = l.args as { quote: Address; creator: Address; bases: readonly Address[]; weights: readonly bigint[]; swapFee: bigint; policy: Address; cap: number };
|
|
1159
|
+
const a = l.args as unknown as { quote: Address; creator: Address; bases: readonly Address[]; weights: readonly bigint[]; swapFee: bigint; policy: Address; cap: number | bigint };
|
|
1166
1160
|
const key: FPoolKey = {
|
|
1167
1161
|
quote: a.quote,
|
|
1168
1162
|
bases: [...a.bases],
|
package/src/history.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
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, type AbiEvent, type Address, type Hex } from "viem";
|
|
16
|
+
import { getAddress, parseAbiItem, 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
|
import { bytea, gql } from "./indexer.js";
|
|
19
20
|
import { blockAtOrAfter } from "./fees.js";
|
|
20
21
|
|
|
@@ -32,7 +33,14 @@ export interface HistoryLog<A = Record<string, unknown>> {
|
|
|
32
33
|
export interface EventLogsQuery {
|
|
33
34
|
/** The rindexer GraphQL endpoint; absent means the chain. */
|
|
34
35
|
indexer?: string;
|
|
35
|
-
|
|
36
|
+
/** The emitter, or several: the four policies share one table per event (indexer phase 1). */
|
|
37
|
+
address: Address | readonly Address[];
|
|
38
|
+
/**
|
|
39
|
+
* The emitter's name in the indexer's config, for an event two indexed contracts share — only
|
|
40
|
+
* needed when the address is not in the client's artifact (a policy the registry approved later):
|
|
41
|
+
* see {@link indexedContractOf}.
|
|
42
|
+
*/
|
|
43
|
+
contract?: string;
|
|
36
44
|
event: AbiEvent;
|
|
37
45
|
/** Indexed arguments to match, as `getLogs` takes them; an array matches any of its values. */
|
|
38
46
|
args?: Record<string, unknown>;
|
|
@@ -70,29 +78,52 @@ export async function getEventLogs<A = Record<string, unknown>>(
|
|
|
70
78
|
|
|
71
79
|
// ─── the indexer path ────────────────────────────────────────────────────────
|
|
72
80
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
81
|
+
/**
|
|
82
|
+
* What the rindexer deployment calls each contract — the prefix of a table two contracts share
|
|
83
|
+
* (logswap-be `devnet/indexer/rindexer.yaml`). The four policies are one factory-tracked contract,
|
|
84
|
+
* `Policy`, except `Listed`, whose two signatures are two contracts (`PublicPolicy`, `PrivatePolicy`).
|
|
85
|
+
*/
|
|
86
|
+
function contractName(c: LogswapClient, address: Address, event: string): string | null {
|
|
75
87
|
const a = address.toLowerCase();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
88
|
+
const is = (x?: Address) => x !== undefined && a === x.toLowerCase();
|
|
89
|
+
const { addresses: d } = c;
|
|
90
|
+
if (is(d.cPoolManager)) return "LogswapManager";
|
|
91
|
+
if (is(d.fPoolManager)) return "FPoolManager";
|
|
92
|
+
if (is(d.router)) return "LogswapRouter";
|
|
93
|
+
if (is(d.policyRegistry)) return "PolicyRegistry";
|
|
94
|
+
if (is(d.tokenFactory)) return "TokenFactory";
|
|
95
|
+
if (is(d.publicPolicy)) return indexedContractOf("public", event);
|
|
96
|
+
if (is(d.privatePolicy)) return indexedContractOf("private", event);
|
|
97
|
+
if (is(d.polPolicy)) return indexedContractOf("pol", event);
|
|
98
|
+
if (is(d.padPolicy)) return indexedContractOf("pad", event);
|
|
79
99
|
return null;
|
|
80
100
|
}
|
|
81
101
|
|
|
102
|
+
/**
|
|
103
|
+
* A policy's name in the indexer for one of its events: `Policy`, or the kind's own contract for the
|
|
104
|
+
* two-signature `Listed`. Pass it as {@link EventLogsQuery.contract} for a policy outside the artifact.
|
|
105
|
+
*/
|
|
106
|
+
export function indexedContractOf(kind: FPoolKind, event: string): string {
|
|
107
|
+
if (event === "Listed") return kind === "private" ? "PrivatePolicy" : "PublicPolicy";
|
|
108
|
+
return "Policy";
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
/** Events whose name two indexed contracts share: postgraphile prefixes their tables. */
|
|
83
|
-
const SHARED = new Set(["Transfer"]);
|
|
112
|
+
const SHARED = new Set(["Transfer", "Approval", "OperatorSet", "CollectorProposed", "CollectorAccepted", "Approved", "Listed"]);
|
|
84
113
|
|
|
85
114
|
const plural = (n: string) => (/(s|x|ch|sh)$/.test(n) ? `${n}es` : `${n}s`);
|
|
86
115
|
/** A column's GraphQL name: postgraphile camelCases the snake_case rindexer writes (`F` → `f`). */
|
|
87
116
|
const column = (name: string) => name[0]!.toLowerCase() + name.slice(1);
|
|
88
117
|
|
|
89
|
-
export function tableFor(c: LogswapClient, address: Address, event: string): { field: string; type: string } {
|
|
90
|
-
const prefix = SHARED.has(event) ? contractName(c, address) : "";
|
|
118
|
+
export function tableFor(c: LogswapClient, address: Address, event: string, contract?: string): { field: string; type: string } {
|
|
119
|
+
const prefix = SHARED.has(event) ? (contract ?? contractName(c, address, event)) : "";
|
|
91
120
|
if (prefix === null) throw new Error(`history: no indexed contract at ${address}`);
|
|
92
121
|
const type = `${prefix}${event}`;
|
|
93
122
|
return { field: `all${plural(type)}`, type };
|
|
94
123
|
}
|
|
95
124
|
|
|
125
|
+
const emitters = (q: { address: Address | readonly Address[] }): Address[] => (Array.isArray(q.address) ? [...q.address] : [q.address as Address]);
|
|
126
|
+
|
|
96
127
|
function decode(type: string, v: unknown): unknown {
|
|
97
128
|
if (v === null || v === undefined) return v;
|
|
98
129
|
if (type.endsWith("[]")) return (v as unknown[]).map((x) => decode(type.slice(0, -2), x));
|
|
@@ -107,7 +138,9 @@ const MAX_PAGES = 200;
|
|
|
107
138
|
const PARALLEL_PAGES = 8;
|
|
108
139
|
|
|
109
140
|
async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
110
|
-
const
|
|
141
|
+
const all = emitters(q);
|
|
142
|
+
if (all.length === 0) return [];
|
|
143
|
+
const { field, type } = tableFor(c, all[0]!, q.event.name, q.contract);
|
|
111
144
|
const inputs = q.event.inputs;
|
|
112
145
|
const condition: Record<string, unknown> = {};
|
|
113
146
|
const filter: Record<string, unknown> = {};
|
|
@@ -164,11 +197,11 @@ async function fromIndexer<A>(c: LogswapClient, q: EventLogsQuery): Promise<Hist
|
|
|
164
197
|
}
|
|
165
198
|
}
|
|
166
199
|
// a table can hold a previous deployment's rows (an indexer not yet reset): keep this contract's
|
|
167
|
-
const want =
|
|
200
|
+
const want = new Map(all.map((a) => [a.toLowerCase(), a]));
|
|
168
201
|
return rows
|
|
169
|
-
.filter((r) => String(r.contractAddress).toLowerCase()
|
|
202
|
+
.filter((r) => want.has(String(r.contractAddress).toLowerCase()))
|
|
170
203
|
.map((r) => ({
|
|
171
|
-
address:
|
|
204
|
+
address: want.get(String(r.contractAddress).toLowerCase())!,
|
|
172
205
|
args: Object.fromEntries(inputs.map((i) => [i.name!, decode(i.type, r[column(i.name!)])])) as A,
|
|
173
206
|
blockNumber: BigInt(r.blockNumber as string),
|
|
174
207
|
blockTimestamp: Math.floor(Date.parse(r.blockTimestamp as string) / 1000),
|
|
@@ -185,10 +218,13 @@ export const LATEST_SCAN_BLOCKS = 5_000n;
|
|
|
185
218
|
|
|
186
219
|
async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<HistoryLog<A>[]> {
|
|
187
220
|
let fromBlock = q.fromBlock ?? 0n;
|
|
188
|
-
const toBlock = q.toBlock ?? (await c.public.getBlockNumber());
|
|
221
|
+
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
|
|
189
222
|
if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
190
223
|
if (q.latest !== undefined && toBlock - fromBlock > LATEST_SCAN_BLOCKS) fromBlock = toBlock - LATEST_SCAN_BLOCKS;
|
|
191
|
-
|
|
224
|
+
const all = emitters(q);
|
|
225
|
+
if (all.length === 0) return [];
|
|
226
|
+
let logs = (await c.public.getLogs({ address: Array.isArray(q.address) ? all : q.address, event: q.event, args: q.args, fromBlock, toBlock } as never)) as unknown as Array<{
|
|
227
|
+
address?: Address;
|
|
192
228
|
args: A;
|
|
193
229
|
blockNumber: bigint;
|
|
194
230
|
transactionHash: Hex;
|
|
@@ -200,10 +236,107 @@ async function fromChain<A>(c: LogswapClient, q: EventLogsQuery): Promise<Histor
|
|
|
200
236
|
}
|
|
201
237
|
if (q.latest !== undefined) logs = logs.slice(-q.latest);
|
|
202
238
|
return logs.map((l) => ({
|
|
203
|
-
address:
|
|
239
|
+
address: l.address ? getAddress(l.address) : all[0]!,
|
|
204
240
|
args: l.args,
|
|
205
241
|
blockNumber: l.blockNumber,
|
|
206
242
|
transactionHash: l.transactionHash,
|
|
207
243
|
logIndex: l.logIndex,
|
|
208
244
|
}));
|
|
209
245
|
}
|
|
246
|
+
|
|
247
|
+
// ─── several events, and the token factory ────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/** A row of {@link getEventsLogs}: one of the asked events, named. */
|
|
250
|
+
export interface NamedHistoryLog extends HistoryLog {
|
|
251
|
+
eventName: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Several events of one contract (or of several emitters sharing them), merged oldest first and
|
|
256
|
+
* tagged with `eventName` — the shape of viem's multi-event `getLogs`, which is what the chain path
|
|
257
|
+
* runs. One indexer table per event; if any of them cannot answer, the whole read goes to the node,
|
|
258
|
+
* so a history is never half the indexer's and half the node's.
|
|
259
|
+
*/
|
|
260
|
+
export async function getEventsLogs(
|
|
261
|
+
c: LogswapClient,
|
|
262
|
+
q: Omit<EventLogsQuery, "event" | "args" | "latest" | "contract" | "transactionHashes"> & {
|
|
263
|
+
events: readonly AbiEvent[];
|
|
264
|
+
/** The emitters' policy kind, to name a shared event's table for a policy outside the artifact. */
|
|
265
|
+
kind?: FPoolKind;
|
|
266
|
+
/** The emitter's name in the indexer (e.g. `PolicyRegistry`), for a contract outside the artifact; `kind` wins for a policy. */
|
|
267
|
+
contract?: string;
|
|
268
|
+
},
|
|
269
|
+
): Promise<{ logs: NamedHistoryLog[]; source: "indexer" | "chain" }> {
|
|
270
|
+
const order = (a: HistoryLog, b: HistoryLog) => (a.blockNumber === b.blockNumber ? a.logIndex - b.logIndex : a.blockNumber < b.blockNumber ? -1 : 1);
|
|
271
|
+
if (q.indexer && q.events.length > 0) {
|
|
272
|
+
try {
|
|
273
|
+
const got = await Promise.all(
|
|
274
|
+
q.events.map(async (event) => {
|
|
275
|
+
const contract = q.kind ? indexedContractOf(q.kind, event.name) : q.contract;
|
|
276
|
+
return (await fromIndexer<Record<string, unknown>>(c, { ...q, event, contract })).map((l) => ({ ...l, eventName: event.name }));
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
return { logs: got.flat().sort(order), source: "indexer" };
|
|
280
|
+
} catch {
|
|
281
|
+
/* the indexer is a cache: the node holds the same events */
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const all = emitters(q);
|
|
285
|
+
if (all.length === 0 || q.events.length === 0) return { logs: [], source: "chain" };
|
|
286
|
+
let fromBlock = q.fromBlock ?? 0n;
|
|
287
|
+
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
|
|
288
|
+
if (q.since !== undefined) fromBlock = await blockAtOrAfter(c, BigInt(q.since), fromBlock, toBlock);
|
|
289
|
+
const logs = (await c.public.getLogs({ address: Array.isArray(q.address) ? all : q.address, events: q.events, fromBlock, toBlock } as never)) as unknown as Array<{
|
|
290
|
+
eventName: string;
|
|
291
|
+
address?: Address;
|
|
292
|
+
args: Record<string, unknown>;
|
|
293
|
+
blockNumber: bigint;
|
|
294
|
+
transactionHash: Hex;
|
|
295
|
+
logIndex: number;
|
|
296
|
+
}>;
|
|
297
|
+
return {
|
|
298
|
+
logs: logs
|
|
299
|
+
.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 }))
|
|
300
|
+
.sort(order),
|
|
301
|
+
source: "chain",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The devnet's token factory event (`src/mocks/TokenFactory.sol`): provenance for every launchable token. */
|
|
306
|
+
export const TOKEN_CREATED = parseAbiItem("event TokenCreated(address indexed creator, address indexed token, string name, string symbol, uint256 supply)");
|
|
307
|
+
|
|
308
|
+
export interface CreatedToken {
|
|
309
|
+
creator: Address;
|
|
310
|
+
token: Address;
|
|
311
|
+
name: string;
|
|
312
|
+
symbol: string;
|
|
313
|
+
supply: bigint;
|
|
314
|
+
blockNumber: bigint;
|
|
315
|
+
/** Unix seconds; set when the indexer answered. */
|
|
316
|
+
blockTimestamp?: number;
|
|
317
|
+
transactionHash: Hex;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Every token the deployment's factory created, oldest first — the token list's provenance (a token
|
|
322
|
+
* named like a factory token but deployed elsewhere is not in it). Optionally one creator's.
|
|
323
|
+
*/
|
|
324
|
+
export async function tokensCreated(
|
|
325
|
+
c: LogswapClient,
|
|
326
|
+
q: { indexer?: string; creator?: Address; fromBlock?: bigint; signal?: AbortSignal },
|
|
327
|
+
): Promise<{ tokens: CreatedToken[]; source: "indexer" | "chain" }> {
|
|
328
|
+
const factory = c.addresses.tokenFactory;
|
|
329
|
+
if (!factory) throw new Error("logswap: tokensCreated needs addresses.tokenFactory");
|
|
330
|
+
const { logs, source } = await getEventLogs<{ creator: Address; token: Address; name: string; symbol: string; supply: bigint }>(c, {
|
|
331
|
+
indexer: q.indexer,
|
|
332
|
+
address: factory,
|
|
333
|
+
event: TOKEN_CREATED,
|
|
334
|
+
args: q.creator ? { creator: q.creator } : undefined,
|
|
335
|
+
fromBlock: q.fromBlock,
|
|
336
|
+
signal: q.signal,
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
tokens: logs.map((l) => ({ ...l.args, blockNumber: l.blockNumber, ...(l.blockTimestamp !== undefined ? { blockTimestamp: l.blockTimestamp } : {}), transactionHash: l.transactionHash })),
|
|
340
|
+
source,
|
|
341
|
+
};
|
|
342
|
+
}
|
package/src/pools.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { signerFor } from "./write.js";
|
|
|
19
19
|
import { readCachedToken, registerCacheClear, writeCachedToken } from "./tokencache.js";
|
|
20
20
|
import { poolId, type PoolKey } from "./keys.js";
|
|
21
21
|
import { presetMintFee } from "./positions.js";
|
|
22
|
+
import { getEventLogs } from "./history.js";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* A market's scalar state, field-for-field with `CPoolState.PoolView` — what the manager's `getPool`
|
|
@@ -162,7 +163,7 @@ export interface DiscoveredMarket {
|
|
|
162
163
|
*/
|
|
163
164
|
export async function discoverMarkets(
|
|
164
165
|
c: LogswapClient,
|
|
165
|
-
opts: { fromBlock?: bigint; toBlock?: bigint; base?: Address; quote?: Address } = {},
|
|
166
|
+
opts: { fromBlock?: bigint; toBlock?: bigint; base?: Address; quote?: Address; /** The rindexer GraphQL endpoint: `Initialize` from there, the node otherwise. */ indexer?: string } = {},
|
|
166
167
|
): Promise<DiscoveredMarket[]> {
|
|
167
168
|
// Narrow the const ABI to the Initialize entry while KEEPING its literal type, so viem can infer
|
|
168
169
|
// the log shape. `as never` would compile but collapse every arg to `never` at the call site.
|
|
@@ -170,28 +171,32 @@ export async function discoverMarkets(
|
|
|
170
171
|
const ev = cPoolManagerAbi.find((x): x is Ev => x.type === "event" && x.name === "Initialize");
|
|
171
172
|
if (!ev) throw new Error("logswap: Initialize event missing from the generated ABI");
|
|
172
173
|
|
|
173
|
-
const
|
|
174
|
+
const args: Record<string, unknown> = {};
|
|
175
|
+
if (opts.base) args.base = opts.base;
|
|
176
|
+
if (opts.quote) args.quote = opts.quote;
|
|
177
|
+
const { logs } = await getEventLogs<{ base: Address; quote: Address; tickSpacing: bigint; swapFee: bigint; x0: bigint }>(c, {
|
|
178
|
+
indexer: opts.indexer,
|
|
174
179
|
address: c.addresses.cPoolManager,
|
|
175
180
|
event: ev,
|
|
176
|
-
args
|
|
181
|
+
args,
|
|
177
182
|
fromBlock: opts.fromBlock ?? 0n,
|
|
178
|
-
toBlock: opts.toBlock
|
|
183
|
+
toBlock: opts.toBlock,
|
|
179
184
|
});
|
|
180
185
|
|
|
181
186
|
return logs.map((l) => {
|
|
182
187
|
const a = l.args;
|
|
183
188
|
const key: PoolKey = {
|
|
184
|
-
base: a.base
|
|
185
|
-
quote: a.quote
|
|
186
|
-
tickSpacing: a.tickSpacing
|
|
187
|
-
swapFee: a.swapFee
|
|
189
|
+
base: a.base,
|
|
190
|
+
quote: a.quote,
|
|
191
|
+
tickSpacing: a.tickSpacing,
|
|
192
|
+
swapFee: a.swapFee,
|
|
188
193
|
};
|
|
189
194
|
return {
|
|
190
195
|
key,
|
|
191
196
|
poolId: poolId(key),
|
|
192
|
-
x0: a.x0
|
|
193
|
-
blockNumber: l.blockNumber
|
|
194
|
-
transactionHash: l.transactionHash
|
|
197
|
+
x0: a.x0,
|
|
198
|
+
blockNumber: l.blockNumber,
|
|
199
|
+
transactionHash: l.transactionHash,
|
|
195
200
|
};
|
|
196
201
|
});
|
|
197
202
|
}
|
package/src/positions.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { FEE_MAX, poolId, type PoolKey } from "./keys.js";
|
|
|
16
16
|
const C_POSITIONS_SLOT = 4n;
|
|
17
17
|
const U128 = (1n << 128n) - 1n;
|
|
18
18
|
import { isPosition, NO_CAP, positionId, unpackFloorCap } from "./ids.js";
|
|
19
|
+
import { getEventLogs } from "./history.js";
|
|
19
20
|
|
|
20
21
|
/** A class's aggregate state — the whole class, not one holder's slice. */
|
|
21
22
|
export interface PositionClass {
|
|
@@ -272,10 +273,10 @@ const ID_REGISTERED = parseAbiItem("event IdRegistered(uint256 indexed id, bytes
|
|
|
272
273
|
* holder's slice is L · shares_owner / shares_class, and fees from the lens. One log query plus
|
|
273
274
|
* a read per registered class; fine for a pool's lifetime of classes.
|
|
274
275
|
*/
|
|
275
|
-
export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n): Promise<HeldPositionRow[]> {
|
|
276
|
+
export async function holdingsOf(c: LogswapClient, key: PoolKey, owner: Address, fromBlock: bigint = 0n, opts: { /** The rindexer GraphQL endpoint: the class registry from there, the node otherwise. */ indexer?: string } = {}): Promise<HeldPositionRow[]> {
|
|
276
277
|
const pool = poolId(key);
|
|
277
|
-
const logs = await c
|
|
278
|
-
const ids = logs.map((l) =>
|
|
278
|
+
const { logs } = await getEventLogs<{ id?: bigint }>(c, { indexer: opts.indexer, address: c.addresses.cPoolManager, event: ID_REGISTERED, args: { poolId: pool }, fromBlock });
|
|
279
|
+
const ids = logs.map((l) => l.args.id ?? 0n).filter((id) => id !== 0n);
|
|
279
280
|
if (ids.length === 0) return [];
|
|
280
281
|
// one lens call: balance, class L, fees, floor and cap for every id the owner holds
|
|
281
282
|
const rows = (await c.public.readContract({
|